An illustration with a lambda symbol between curly brackets
Blog

The perils of functional CSS

Functional CSS is a contentious topic, and one that regularly generates heated comment thread debate. In such situations, it can be tricky to tease out the hyperbole from the measured opinion.

Here’s my view on the subject, based on my recent experimentations with the approach during a project to build a web application.

First, what is functional CSS?

Functional CSS (sometimes referred to as atomic CSS) is the practice of using small, immutable and explicitly named utility classes to construct components. A variety of stock, ready-to-go class libraries, such as Tachyons and the increasingly popular Tailwind CSS, have sprung up to make starting with functional CSS as easy as possible.

So, instead of creating a custom component, semantically or visually named, such as:

<div class="card">A card</div>

You would construct the component’s styling rules within the markup through a combination of utility classes. For example:

<div class="padding-sm border-radius-sm bgc-white"></div>

In the same way that functional programming encourages purity and predictability, functional CSS promotes the use of single-purpose and ‘pure’ classes that consistently behave the same regardless of context (or ‘scope’ in traditional programming).

A good litmus test for whether a class is ‘functional’ or not is checking if the result of using it is immediately discernible from its name. Additionally, you can ask whether or not that name only applies a single style rule (although there are exceptions).

Functional CSS can be seen as a rebellion against the age-old best practice of the ‘semantic’ class name – such as ‘news-item’ or ‘greeting’ – long espoused by sites such as the famous CSS Zen Garden and notable developers such as Jeffery Zeldman.

Screenshot of CSS Zen Garden

Instead, functional CSS argues that there is no point trying to adhere to a false ‘separation of concerns’ between HTML and CSS, since, as the Vue.js docs note:

‘…separation of concerns is not equal to separation of file types.’

To write genuinely re-usable CSS, we already enter into visual and functional abstractions, such as ‘card’ instead of ‘news-item’, or ‘media-object’ instead of ‘author-profile’. This is the premise for all UI libraries, from Bootstrap through to Semantic UI. As Adam Wathan, author of Tailwind CSS notes:

‘There’s no pretending that .stacked-form is any more ‘semantic’ than .align-right; they’re both named after how they affect the presentation of the markup, and we are using those classes in our markup to achieve a specific presentational result.’

Functional CSS takes this to the extreme by encouraging us to abandon these abstractions entirely. Instead of trying to come up with names for components that are potentially never re-used, a functional approach suggests we use the immutable utility classes of our chosen library, meaning you may never have to write your own CSS again.

The problems

Context

Perhaps the most marked issue – and in some cases feature – is how counter a functional approach runs to context-aware nature of CSS.

While methodologies such as BEM and SMACSS promote a flat class hierarchy, limiting specificity, there are many occasions where contextually aware rules make sense. Take a news list which switches display based on a parent class:

<ul class="summary-list">
    <li class="summary-list__item">A list item.</li>
</ul>
<ul class="summary-list summary-list--compact">
    <li class="summary-list__item">A list item.</li>
</ul>

The alternative would be iterating through each of the news items and adding a summary-list__item--compact class to each (or the functional set which makes up its equivalent), however, there would be a performance penalty looping over each item, and you would also lose the ability to easily ‘hook’ into the compact view for additional changes (e.g. switching the parent display from grid to block).

Screenshot showing the eBay toggle functionality.
How would you replicate a display-mode toggle with a functional approach?

Theming is similarly problematic. Traditional functional classes such as text-black have to be mapped and changed per-element, which is a performance nightmare. Imagine changing the meaning of functional classes based on their context – it would not only be confusing but also next to impossible, due to their widespread non-specific use.

You can, of course, escape from these issues by creating your own aliases, such as primary/secondary etc:

.text-primary { @apply .text-black; }
.t-dark .text-primary { @apply .text-white; }

Or even better, use aliasing alongside CSS variables if your browser-support requirements permit:

::root { --t-primary: black; }
.t-dark { --t-primary: white; }
.text-primary { color: var(--t-primary); }

Regardless, the second you alias or squirrel away the explicit functional meaning of the class into abstract concepts, be it primary/secondary or ‘featured’ etc, you have already removed the functional aspect.

Indeed for some proponents that may actually be the point. As the author of Tachyons, Adam Morse, notes:

‘Having a class that has multiple definitions being redefined depending on context doesn’t solve problems, it creates them.’

However, this is only a problem if the class name is stylistically descriptive, such as float-left, with the context changing the specific meaning. Contextually changing a module that is defined either semantically, e.g. news or within a UI construct, e.g. card is likely to be considerably less problematic than Morse suggests with his example:

‘Imagine if you had a function called ‘filesize’ where you passed it a filename and it returned the filesize. That sounds pretty great. But imagine if sometimes it returned the amount of lines in the file. That doesn’t sound great at all does it.’

There is also a different kind of context to consider, which is the use of relational selectors such as direct-descendant (>) and adjacent sibling (+). These enable modules to base their appearance on their surroundings, and have been the building blocks for techniques such as the infamous lobotomised owl selector:

ul > * + * { margin-top: 10px; }


Functional CSS traditionally sidesteps these approaches for a more explicit template-based class-or-no-class approach, where you would simply not include the margin class on the final or first item:

<ul>
    <li>Apple</li>
    <li class="margin-top-10">Orange</li>
    <li class="margin-top-10">Pear</li>
</ul>

Not only is handling this in templating a chore, if an item is dynamically added or removed with JS, you’ll also have to adjust the classes manually.

Of course, nothing is stopping you creating a spacing utility class or module, but once again this moves us closer to the ‘content-agnostic utility classes’ middle-ground that Wathan describes on his journey to today’s ‘functional’ approach.

Responsive Design

As part of a functional CSS process, classes are pre-defined, meaning you’ll also be defining your fixed breakpoints up front (such as ‘sm’/’md’/’lg’). Unfortunately, such an approach doesn’t gel with modern responsive design as we know it.

Specific ‘macro’ breakpoints have long fallen out of favour, with a leaning towards component-specific breakpoints or ‘tweaks’ as and when a component no-longer looks correct. With techniques such as the Flexbox Holy Albatross and the rise of ‘intrinsic’ web design, not to mention the inevitable native support for some form of container queries, this feels very out-dated. At a time where we are desperate for less homogeneity and more component-out design, it feels weird to be tying module display to specific macro breakpoints.

On top of this, tools such as CSS-grid don’t make sense when using a functional CSS library because the rules for interesting grid definitions tend to be so specific you’d likely end up writing a component to make them work anyway.

Pseudo-elements and selectors

As pseudo-elements don’t exist in the HTML in a traditional way, functional CSS can’t target them effectively, which is problematic. Indeed, there’s no real solution for this issue that doesn’t involve either a component abstraction or some kind of duplication of rule definitions.

For instance, Wathan recommends registering a ‘variant‘, which would give you an additional class, such asbefore-text-red. The fix is then to place this class on the component containing the ‘before’ rule, but once you’ve gone to this length, you’re essentially adding utility-classes to the parent, which do not apply directly to it.

Hacky solutions

Perhaps the least technical but potentially most pervasive issue with functional CSS is that it merely makes developers lazy.

If the best solution to a problem is a bespoke component, then you should probably be making a bespoke component. With it’s dogmatic adherence to the simplicity of utility class libraries, functional CSS arguably gives developers an all to easy excuse to not bother creating original components.

The good bits of functional CSS

Performance

There’s no denying that adhering to a functional CSS approach can bring some performance benefits, since the actual amount CSS you’ll need to ship creating projects in this way is small. The overall impact is likely to be subtle for the average site or web app, but it’s certainly a nice-to-have bonus. Plus, since you’re updating the CSS less frequently, you’ll benefit from a more cache-friendly stylesheet.

However, Nicholas Glasser argues that this benefit is something of a red herring:

‘Experienced CSS developers using pre-processors don’t need to be overly concerned about a certain degree of repetition in the compiled CSS because it can lend itself well to smaller file sizes after HTTP compression. The benefits of more maintainable “CSS” code via pre-processors should trump concerns about the aesthetics or size of the raw and minified output CSS.’

Regardless of your thoughts on the impact of a smaller CSS, there can also be a human element to the performance benefits of moving to a functional approach. For example, developer onboarding is more straight-forward, and productivity across projects can increase drastically thanks to the fact that your team is sharing a library or codebase. This isn’t a feature unique to functional CSS (the same would be true of Bootstrap, for example), but it’s still a potential win if the circumstances fit.

Secondly, functional CSS makes it simpler for team members outside of a core front-end team to make changes to a given site or project. With it’s reduced CSS writing overhead and shunning of abstractions, it’s entirely possible for a back-end dev to make changes to a project independently, which can be useful in a small team.

Consistency

Viewed from a particular perspective, the pre-defined values offered by the functional approach can have real-world benefits for consistency across a project. Wathan, again, spells this out succinctly, suggesting that:

‘When everyone on a project is choosing their styles from a curated set of limited options, your CSS stops growing linearly with your project size, and you get consistency for free.’

This could be of particular interest in a team with high churn, such as one that employs a lot of contractors or freelancers, or when moving responsibility for a project between teams (or even between external agencies for companies without their own in house dev). However, Sarah Dayan suggests that code or layout inconsistency is mainly a ‘human issue, not a technical one’, and even adhering to a functional CSS approach won’t matter ‘if you don’t follow the rules, style guides and best practices that your team put in place’.

Why not have a bit of both?

Screenshot of a CSS Tricks article about using functional CSS alongside traditional CSS.

Despite the tension between both sides of the argument, there is a growing convergence towards adopting both a component and functional approach into what Tailwind has dubbed ‘utility-first’:

‘The reason I call the approach I take to CSS utility-first is because I try to build everything I can out of utilities, and only extract repeating patterns as they emerge… Taking a component-first approach to CSS means you create components for things even if they will never get reused. This premature abstraction is the source of a lot of bloat and complexity in stylesheets.’

The abstraction of components into discrete entities only comes after there is a specific need for it, i.e. a repeating UI pattern that will in some way benefit from a more traditionally ‘semantic’ grouping. In Tailwind this kind of composition would look something like this, not unlike a traditional preprocessor mixin:

.btn {
  @apply font-bold py-2 px-4 rounded;
}

While utility classes have their place, and when used in moderation can be extremely useful, this kind of ‘composition’ surely negates any benefit gained over traditional component-driven CSS. If a button is worthy of abstraction, it is hard to imagine many UI components not worth abstracting that are genuinely ‘one-time-use’, like the nav-bar example Adam gives.

In fact, the second you have composed a class into an abstraction, you have lost:

  • The explicitness of the class name.
  • The benefits of not having to name or devise a component.
  • The time saved by not having to write your own CSS.

Worse still, the extensive combination of both component classes and functional utility classes actually makes it harder to understand, and creates a further developer burden when it comes to deciding what is a worthy and re-usable modification on a class and what should remain a one-time variation:

<!-- Should this be 'card--large' and 'card--light' ? -->
<div class="card bg-white padding-lg">Card</div>

This kind of intermingling has the potential to be extremely destructive, especially when newer or less experienced developers use functional classes as an escape hatch to ‘undo’ component rules (e.g. increase the padding defined in ‘card’).

The only way to ensure readability and maintainability is to be extremely strict with the application of both so that they do not intersect or share concerns:

<div class="bg-white padding-md flex">
    <div class="profile">Jay Freestone etc.</div>
</div>

Conclusion

In summary, functional CSS doesn’t make much sense to me, as a front-end developer. I’d suggest that a dedicated front-end team would be more productive managing their own CSS approach, rather than being tied into the straitjacket of a standardised class library.

However, if you’re not great with CSS and struggle to come up with appropriate abstractions, or if you don’t have a dedicated front-end team to handle this end of things, I can see how functional CSS would be useful. I don’t think it’s a coincidence that back-end developers have written most functional CSS libraries.

If, as a front-end dev you do think functional CSS can fix a problem you or your team are facing, my advice would be first to consider whether the problem is actually a more human one. I think this quote from Zeldman says it well:

I don’t believe the problem is the principle of semantic markup or the cascade in CSS. I believe the problem is a dozen people working on something without talking to each other.