I’ve been honing down on my personal preferred coding style lately, and I think I can sum it up as less. Do more with less. Less JS. Less CSS. Less HTML. Less code is generally more maintainable code.
The quality of ‘maintainable’ is largely subjective, I’ve come to believe that what we perceive to be maintainable is what we perceive to be familiar. And what’s familiar varies from person to person, and even from week to week for the same person. The code you wrote last month is less maintainable than the code you wrote last week. The best way to write familiar code is to write as little as possible, and embrace the standards of the open web everywhere else.
This weekend got me thinking about that, after I made a somewhat controversial PR to the Maybe codebase to solve a problem I had identified when working on a styling fix to the form fields.
The Maybe application uses Tailwind CSS, a “utility-first” CSS library, and my PR cut across the grain of their recommendations. I did this, not because I dislike Tailwind (far from it), but because I believe when you mix utility classes in with a handful of CSS components you get a solution that is highly composable and reduces the amount of code you have to write and maintain.
Let me explain the problem at hand: these form elements are styled in a way that the wrapping container, the label and the input have a set of style rules that are tightly coupled. One could not work without the other, and together in that arrangement they form a complete whole. These styles were also duplicated across every form field in the application. This was certainly a case where we needed to avoid repetition early on before the pain of updating the styles across every field becomes too great.
So what were the options, and what did I propose?
Option 1: Extracting components and partials
This is a direct recommendation from Tailwind’s documentation: “If you need to reuse some styles across multiple files, the best strategy is to create a component, or a template partial”.
Since we’re building in Rails we would build a template partial. The problem here is this approach feels very heavy-handed for what is essentially a div, a label, and an input. On top of that, the input should be rendered by our form, we would need to build some version of a solution that allows us to pass the correct form field and input type into our component partial while also applying our styles to that input.
Since we’re building in Rails we would build a template partial. The problem here is this approach feels very heavy-handed for what is essentially a div, a label, and an input. On top of that, the input should be rendered by our form, we would need to build some version of a solution that allows us to pass the correct form field and input type into our component partial while also applying our styles to that input.
Option 2: Rails Helpers
Another approach in spirit with this recommendation might have been to create a set of helper methods that either return the classes, or the required markup directly. This certainly would have solved the concerns around the partial being too heavy-handed, though it still leaves us trying to solve some problems. Either, we’ll need to ensure our classes are added to every wrapper, label and input. Or, we’ll need to figure out how to pass the correct form field and attributes down again through the helper.
Option 3: FormBuilder
Fortunately, Rails comes with native support for a concept it calls FormBuilder. It allows us to create our own FormBuilder which can replace the default implementation and it allows us to modify the behaviour so we can patch in our own styles, or other attributes.
It wasn’t the approach I chose to propose, but I’ll touch more on this later.
Option 4: Extracting classes with @apply
This is another direct recommendation from Tailwind’s documentation: “You can use Tailwind’s @apply directive to extract repeated utility patterns to custom CSS classes when a template partial feels heavy-handed”
Now I could have sworn a few years ago they were recommending to avoid using the @apply syntax, but whether they did or didn’t, the syntax still feels off to me. There are two benefits I can see: You get access to your design tokens, and you get to use the same syntax you use in your HTML. I'd be happy to use it but it's not my preference.
The example of this approach in their documentation however is a single element, a button, it doesn’t entirely show us how @apply might work for a small component composed of 2-3 elements. Should each have it’s own classname, or should we use CSS selectors to select the sub elements?
Proposal 1: Extracting a class with theme()
https://github.com/maybe-finance/maybe/pull/272
We finally arrive at the solution I decided to propose, I’ll post it below for you to check out.
.form-field { position: relative; border: 1px solid theme("colors.gray.100"); background: theme("colors.offwhite"); border-radius: theme("borderRadius.xl"); &:focus-within { background: theme("colors.white"); box-shadow: theme("boxShadow.DEFAULT"); opacity: 1; } label { display: block; font-size: theme("fontSize.sm"); font-weight: theme("fontWeight.medium"); color: theme("colors.gray.700"); padding: theme("spacing.4"); padding-bottom: 0; } input { padding: theme("spacing.4"); padding-top: theme("spacing.1"); background: transparent; border: none; opacity: 0.5; outline: none; width: 100%; &:focus { opacity: 1; @apply ring-0; } } }
The theme() function exported by Tailwind allows us access to our design tokens, it’s not my preferred way to access my design tokens - I typically write a Tailwind plugin to export them into CSS variables - but it does the same job, and this project uses the standalone Tailwind CLI to avoid having to worry about a dependency on node or having to npm install.
I believe this proposal was controversial on three counts.
First, I didn’t use the @apply syntax, what is this alien CSS code? I don’t have any strong logical reasoning for this other than my grugbrain telling me @apply with all the rules on one line looks ugly, and a line per rule is more readable. (I actually did use @apply ring-0 because this utility does more than just change a property - it also sets some tailwind specific custom properties.)
Second, I dared to use nesting and on top of that I used element selectors. I guess this is what feels in complete contradiction to what the approach Tailwind propose, the only example of @apply they show is with a single element, a button.
As previously mentioned this collection of styles for the field form a whole. You would not want to use one of these without the others. I also wouldn’t want to have to ensure I apply this class to each element where I want this style to appear. One time is enough. Nesting also reduces our risk of class name collisions, a generic .label utility may get created in different contexts. We also create a single namespace to grep our concept with.
Also, since most of the work is already done for us through utility classes and we’re tightly scoped by our block class we can also just select the raw elements without having to apply a class name to each.
Third, it created a path forward for future abstractions with no clear philosophy or guidance. Which is perhaps the most concerning of the issues raised here. An absolute and fundamental rule of Tailwind, but also my own philosophy is to avoid premature abstraction.
In order to write less CSS we must abstract only when necessary. By using CSS the right way we assign most of our rules at a higher level, typically supported by global CSS, composition classes and utility classes. Components or blocks as I like to refer to them by are the lowest level, and by the time you get to them most of the work should have been done.
I couldn’t give you an exact number on how many abstractions is too many, or just the right amount, it depends as always on the size and complexity of your application. But it should be a small number.
The size of the abstraction also matters, a block should be a handful of CSS rules and shouldn’t grow larger than 80 - 100 lines. Each block should solve a single contextual problem, and the internals of that block should be handled as a separate concern.
Some common abstractions include buttons, form fields, boxes, panels, etc. Elements that are formed of 1 - 4 elements and may be re-used in several places.
By following these principles we make our CSS more composable, and we can write less of it.
Proposal 2: FormBuilder
https://github.com/maybe-finance/maybe/pull/290
After putting the first proposal out there and getting some comments and feedback it felt like there a level of consensus that the FormBuilder approach may be more suitable for this specific problem involving the form fields.
While I still believe the first approach could have been the best solution, this isn’t my project and I’m always happy to try out an alternative path.
FormBuilders have a strange allure to them, they feel like the perfect solution for this problem. However, I find they’re also a difficult beast to tame. The problem with FormBuilders is the technical complexity immediately ramps up if you’re not careful.
Before we could generically target the input element, or a class applied to the input element. Now we have to choose to define a method for each and every field type: def text_field, def email_field, def password_field... Either that, or we embrace meta-programming and define these fields dynamically, that’s how the base FormBuilder class does it for the majority anyway.
Meta-programming is great but it’s not something I want my brain to have to wrestle with when I’m updating some styles on a form. So I spend some time figuring out a good abstraction for it, while further increasing the complexity and length of the solution.
And while I’m building out this solution, in the back of my mind all I can think about is the complexity of extending or maintaining this solution. At some point someone will likely want to come in and add styles for error states. Or, maybe there will be one field in the form that needs to be styled differently. What if the field wants to build upon the default styles, adding further utility classes to the element.
With this solution we’ve lost a level of composition we had before, and styles are now more tightly coupled than before. We would have less CSS, but traded for more code elsewhere, with increased complexity.
Conclusion
Neither of the proposed solution have been merged at this point, but I wanted to note down my thoughts on this topic whilst they’re still fresh. If one of the solutions gets merged I'll perhaps come and update this post.
Thank you to those who reviewed and commented on the solutions in GitHub. Contributing to any open source project is a delightfully eye opening experience as to how many different ways we all come up with for solving similar problems.
Thank you to those who reviewed and commented on the solutions in GitHub. Contributing to any open source project is a delightfully eye opening experience as to how many different ways we all come up with for solving similar problems.
Finding the right abstraction is hard, and while we all have our own personal preferences, ultimately maintainability is in the eye of the beholder (in this case the core maintainers and owners of the Maybe application). Maintainable code that anyone can jump in and work on really is a holy grail.
CSS remains one of the most complex languages in our ecosystem, I often see the terms cascade or inheritance misused or misunderstood. It is not enough just to know the syntax of CSS but to master it you must also understand how it works without the help of error messages or exceptions like other regular languages.
CSS remains one of the most complex languages in our ecosystem, I often see the terms cascade or inheritance misused or misunderstood. It is not enough just to know the syntax of CSS but to master it you must also understand how it works without the help of error messages or exceptions like other regular languages.
My current personal philosophy on CSS is heavily inspired by the Cube CSS Methodology by Andy Bell, and makes heavy use of global styles, composition classes, utility classes generated by Tailwind CSS, and of course a handful of re-usable blocks. It let's me write less, and do more. I highly recommend anyone interested to check it out along with the website https://buildexcellentwebsit.es/.