Functional CSS is great, except when it isn’t (Or why you shouldn’t use functional CSS in your large scale application)

Carrie Becker
Stories from the Herd by Tucows
8 min readJul 7, 2023

--

Functional CSS is an intriguing concept. It’s easy to get started, you can scaffold a project very quickly, and everyone reading your markup knows exactly what’s going on, at least on a superficial level. On the surface it sounds like all benefits with no catch, but there’s a catch. There’s always a catch.

Close up of a child’s robot constructed from snap-together toy construction bricks.

What is Functional CSS?

Functional CSS, also known as atomic CSS, breaks down styles into small, static chunks that can then be applied on a wide selection of elements. Rather than having a specific class to describe each unique element type, functional CSS uses general classes to describe base attributes.

For example, you might be used to seeing style declarations like this:

.primaryButton {
border: 1px solid grey;
background-color: white;
color: violet;
}

Functional CSS would break it down into several style declarations that can be used together on your primary button or separately on other elements:

.border {
border: 1px solid grey;
}
.bg-white {
background-color: white;
}
.txt-purple {
color: violet;
}

Functional CSS is very common in open source component libraries and content management systems because it’s so easy to use. It’s an easy plug and play solution for when you don’t want to build your own style library, or can’t for whatever reason. But I mentioned a catch. Functional CSS is not very scalable. And if it’s not scalable, it’s not a good fit for most large-scale, future-proof projects.

What is Scalability and why does it matter?

Scalability is the ability to accommodate growth. More specifically for our purposes, this means the ability to adapt to and accommodate changes in design and markup. Scalability is vital in large, on-going projects. A scalable project allows for clean and efficient maintenance of existing code bases. It is flexible enough to handle design changes, from small tweaks to major revisions without requiring equally large rewrites. Finally, scalability is linked to responsiveness and performance, with scalable CSS providing a faster, more consistent experience for end users.

Class Overload

I’ve mentioned already how easy it is to scaffold a project using functional CSS. Getting prototypes, or a limited number of pages with limited variations up and running with functional CSS makes sense. What happens afterwards is the problem. In most cases, as a project grows, the styles grow: more components, more variety between components, more use cases for components.

In a small project you might have three or four font sizes. Large projects almost always have more than that. Same with colours, white space rules, shadows, you name it. The larger the project grows, the harder it becomes to remember which font-size, or colour, or padding belongs to which use case and how they combine together. Even if you abstract the names away from font-size-14 to font-size-small, you still need to remember if the paragraphs in that marketing card are small or medium, or if the call to action buttons in the header banners need the level 1 shadow with the paragraph front or level 2 shadow with the header font. You either have to have an incredible memory or a style guide open beside you at all times.

Your call to action button goes from looking like this:

<button class='btn-primary btn-cta'><button>

to looking something like this:

<button 
class='bg-violet txt-white font-size-small font-style-caps shadow-lvl-2 display-flex-center'
><button>

The classes get even more intense when you need to mix responsiveness into them. To do functional CSS cleanly, your classes need to match exactly what it looks like. So if an element needs to change from full width to half width when the screen size changes, the classes need to include that. The larger the project, the more breakpoints it has and your button ends up looking like this:

<button
class='bg-violet txt-white font-size-small font-style-caps shadow-lvl-2 display-flex-center media-phone-width=full media-tablet-landscape-width=half-min-content media-desktop-width=content'
><button>

But perhaps you solve the above problem with a tightly coupled custom component library that applies the classes for each use case through a passed property. You’ve abstracted away the long string of classes into a tidy single representative property … only to undo that abstraction when you apply the classes inside the component with if or switch statements, or imported constants.

And if you’re using looping or repeating elements, like lists, tabs, or tables, you’re being crushed with repeated classes on every single list item, and your accessibility attributes are nearly unfindable .

<button class='bg-aqua txt-white padding-10 margin-inline-10 align-left weight-500 width-1fr border-radius-top-10 hover-bg-aqua-110 active-bg-aqua-120' aria-role='tab' aria-controls='daisy-tab' aria-selected='true'>Daisy</button>
<button class='bg-aqua txt-white padding-10 margin-inline-10 align-left weight-500 width-1fr border-radius-top-10 hover-bg-aqua-110 active-bg-aqua-120' aria-role='tab' aria-controls='petunia-tab' aria-selected='false''>Petunia</button>
<button class='bg-aqua txt-white padding-10 margin-inline-10 align-left weight-500 width-1fr border-radius-top-10 hover-bg-aqua-110 active-bg-aqua-120' aria-role='tab' aria-controls='gladiolus-tab' aria-selected='false''>Gladiolus</button>

All these classes are taxing for you to scan and keep track of, and they can bog down your app. Assistive technologies scan your app as well. They need to read each and every class despite the classes not providing any useful context. The javascript used to parse the abstracted property back into functional classes clutters your code and puts the design layer firmly back in the functional layer. It’s an awful lot of work when you can just apply the abstracted property as the class and use CSS’s built-in parsing to decide which styles should be applied.

Future Proofing

Scalability dictates how well an application can carry on into the future. We’ve already looked at how your markup can become nearly unreadable with classes and how memorizing which classes applies to which situation gets exponentially harder as the app grows but what about the future?

No one likes change. But functional CSS hates it even more. Let me illustrate. Your designers have a 10pt white space system, and have set the main colours to shades of violet and aqua. You’ve gone beyond most functional CSS frameworks to abstract away the actual colour names into primary and secondary, and the white space system into multiples (.padding-1x, .padding-2x, etc).

You believe this covers you in case design changes the system to a different point system and swap out the colours. You just need to go into your CSS, change the padding and margin class rules to use a different multiple and switch the colour codes in your colour classes.

But now design is asking to change to a 7pt system and change most, but not all, white space instances from 2x to 3x. Further, they’ve changed their subtle shading colour from 10% of the default violet to 20% of default aqua. Now you have to find and replace all the .bg-primary-10 classes to .bg-secondary-20 and find the relevant .padding-2x and replace them with .padding-3x (Same for margin, by the way).

And what if design wants you to change the text colour from grey-10 to white on all the instances of a td that has the following classes:

<td class='bg-aqua-10 border-aqua-50 txt-grey-10 padding-10 align-centre weight-300 font-size-medium'></td>

Easy, you think. Just search and replace. But that’s assuming you’ve been very meticulous about having all your classes in the same order every time. And you’ve been very meticulous about not forgetting any classes ever. As a project grows, so too does the size of the team. No one can ever miss or reorder a single class or else you can no longer search and replace when a change is required.

Every time you mix in presentational values into your markup, you run the risk of having to scour your markup when the design inevitably updates. Keeping your presentation in your presentational layer and referencing it with abstracted classes allows you to update styles much less painlessly.

The future isn’t just about change. It’s about growth. With the advent of CSS custom properties, theme switching has become a lot easier. You can switch from dark to light with a button. Maybe you turned your app into a product to sell or share open source and you need to have a way to apply custom themes. If your app is littered with functional classes like font-size-small or worse font-size-14, applying themes is a fight against your styles.

Take for example the case of theme-switching from dark to light. Your dark-mode button is white with violet text. Now the classes no longer describe the style because recall what our button looks like:

<button class='bg-violet txt-white font-size-small font-style-caps shadow-lvl-2 display-flex-center'><button>

But your primary button class will always be semantic no matter what theme you’ve switched to. And to spare yourself growing pains, use custom CSS properties to define your primary style and swapping out colours with javascript is a semantic breeze:

.btn-primary {
background-color: var(--primary-bg-color);
color: var(--primary-color);
}

What should I do Instead?

Keep using functional CSS for prototypes, one-and-done type projects, mini projects, and desperate mad-dash projects that need to be done yesterday. But for larger projects that you want to keep around far into the future, that will have developers come and go, try something a little more abstract. OOCSS is an option. BEM is also nice. I personally use a slightly less verbose version of BEM. Do what feels comfortable but be consistent about it. The important thing to remember is to keep your presentational layer only abstractly connected to your functional layer.

Key things to consider when choosing a CSS methodology to choose are:

  1. How does it scan? Can you scan your markup in the developer tools or an editor and find what you’re looking for? Can new team members quickly and easily find relevant elements?
  2. How does it perform? Consider both raw performance and assistive technologies. Depending on your target audience, you may need to allow for slower connections or older devices.
  3. How does it handle change? Consider the level of effort to update the styling of commonly used elements. Does it require finding all instances of that element and changing it or can it be done in a single place? Consider the mental load of inserting an instance of an existing element. Do you need to remember specifics or are the classes intuitive to learn and remember?
  4. How does it develop and maintain? Do other attributes get lost amid the classes? Consider how easy or difficult it will be to maintain proper Aria attributes, event handlers, ids, tab-indexes, among other things when every element has potentially upwards of 10 classes.
  5. Don’t forget about CSS custom properties and CSS preprocessors. Using a combination of both can functionalize your CSS while allowing you to keep your concerns separate.

--

--

Interface developer since 2011 focused on sustainable & scalable front end architecture and accessibility advocacy