Back

The Shorthand-Longhand Problem in Atomic CSS

14min • 14 December 2020

In this article I'd like to talk about the biggest caveat of Atomic CSS - precisely Atomic CSS generated by CSS-in-JS libraries. It's about CSS specificity, shorthand and longhand properties in CSS and how those libraries generate Atomic CSS.

Terminology

Before I dive deeper, I'd like to briefly explain the terminology.
It's important to know these concepts to understand the problem space.

CSS Specificity

Let's check the definition on MDN(new tab):

"Specificity is the means by which browsers decide which CSS property values are the most relevant to an element and, therefore, will be applied. Specificity is based on the matching rules which are composed of different sorts of CSS selectors(new tab)."

To put it in simple words: It's how browsers decide which styles are more important than others and eventually overwrite those. While the whole calculation is quite complex, luckily we only care for the most simple scenario: single and multiple selectors. More on that later on.

The Cascade

In CSS, if the specificity of two rules is equal, those rules are applied in the order that they're defined in the stylesheet. This is known as the cascade. Let's look at an example:

.first {
color: red;
}
.second {
color: blue;
}

If we pass both class names to an element, it will always prioritize the second overwriting the first styles.

Extra Tip: A common misconception about CSS is that the order in which class names are passed to an element matters, but that's actually not true. The order in which they are defined is what matters in terms of the cascade. Therefore, <div class="first second"> and <div class="second first"> are the same.

Shorthand & Longhand Properties

In CSS there are so called shorthand and longhand properties. Shorthand properties, as the name implies, are short versions for a set of longhand properties. For example, the most common ones are probably padding and margin. Instead of setting each separate directional padding:

.class {
padding-top: 5px;
padding-right: 10px;
padding-bottom: 15px;
padding-left: 20px;
}

we can use the padding shorthand to define all values with a single property:

.class {
padding: 5px 10px 15px 20px;
}

In modern CSS, we have more than 20 shorthand-longhand pairs. Despite the initial overhead of learning those shorthands, the majority of people seem to like them.

Extra Tip: Another common misconception about shorthand properties is that the order of values matters. While this is true for directional shorthands such as padding, margin, border-width, border-style and border-color, it actually doesn't matter for the rest. For example, border: 1px solid black is the same as border: solid 1px black.

Atomic CSS

When talking about Atomic CSS, we're actually talking about the methodology that defines how CSS classes are written - not about the actual framework Atomic CSS(new tab) by Yahoo.

You may have heard about BEM(new tab) or OOCSS(new tab) - it's similar to those, but with different tradeoffs. It is similar to utility-first CSS, which became popular with frameworks like Tailwind(new tab), but even more extreme and restricted.

In Atomic CSS, every property-value is written as a single CSS rule. Let's take a standard example:

<div class="class">Hello</div>
.class {
padding-left: 10px;
font-size: 16px;
color: red;
}

This would now be written as something like this:

<div class="pl-10 fs-16 c-red">Hello</div>
.pl-10 {
padding-left: 10px;
}
.fs-16 {
font-size: 16px;
}
.c-red {
color: red;
}

As this post is not about the benefits and tradeoffs of different CSS methodologies, I'm not going to dive any deeper here. All we need to know, is how it works in general.

Atomic CSS-in-JS Libraries

With the rise of CSS-in-JS libraries(new tab), we got new ways to optimize the rendered CSS output. One of which is to automatically generate Atomic CSS classes.

Consider this pseudo code:

import css from 'css-in-js-library'
// => a b c
const className = css({
paddingLeft: 10,
fontSize: 16,
color: 'red',
})
.a {
padding-left: 10px;
}
.b {
font-size: 16px;
}
.c {
color: red;
}

It allows us to write our styles in a familiar "monolithic" way, but get Atomic CSS out. This increases reusability and decreases the final CSS bundle size. Each property-value pair is only rendered once, namely on its first occurrence. From there on, every time we use that specific pair again, we can reuse the same class name from a cache. Some libraries that do that are:

In my honest opinion, I think that this is the only reasonable way to actually use Atomic CSS as it does not impact the developer experience when writing styles. I would not recommend to write Atomic CSS by hand.

The Problem

Now that we know all about the terminology and how Atomic CSS and CSS-in-JS libraries work, we can dive right into the problem space. In a "monolithic" system, where all styles are rendered to a single CSS class, the order of properties always determines their priority.

For example, using a "monolithic" CSS-in-JS library, the following styles will always apply a final padding of 10px 5px 10px 5px:

import css from 'css-in-js-library'
// => _hash
const className = css({
padding: 5,
paddingTop: 10,
paddingBottom: 10,
})
._hash {
padding: 5px;
padding-top: 10px;
padding-bottom: 10px;
}

But if we do the same with an Atomic CSS-in-JS library, we don't get the same deterministic solution:

import css from 'css-in-js-library'
// => a b
const first = css({
paddingTop: 10,
paddingBottom: 10,
})
// => c a b
const second = css({
padding: 5,
paddingTop: 10,
paddingBottom: 10,
})
.a {
padding-top: 10px;
}
.b {
padding-bottom: 10px;
}
.c {
padding: 5px;
}

This renders the expected results if we only render the second styles, but as soon as we render first prior to that, it will break.

Why? If the longhand properties are rendered prior to the shorthand, they'll always end up above the shorthand in our rendered CSS. As we already learned, the order of CSS classes determines how they're applied due to the cascade.

All of a sudden, the order in which the styles are rendered, matters.

Possible Solutions

Being the creator and maintainer of Fela, one of the above mentioned CSS-in-JS libraries with Atomic CSS output, I have a very personal interest in solving this problem. Let me explain all the possible solutions we've tried, including all of their benefits and tradeoffs.

Developer Discipline

The first and most obvious solution is to just not use shorthand properties at all. Doing that, we completely eliminate the problem.

The only remaining problem is that this approach requires serious amounts of discipline to do that. Remember, most people are familiar with shorthands. They like and use them regularly. Now we want them to stop doing that? It becomes even more problematic if we're talking about a team of people working on the project simultaneously.

Knowing that, I'd argue that this is not a good solution to our problem, so let's go ahead to the next iteration.

Warnings & Linting

If we can't rely on discipline alone, we might be able to codify the conventions. It shouldn't be too hard to validate an object accordingly, e.g.

validateStyle.js

const properties = {
padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
// ... all other properties
}
export function validateStyle(style) {
for (const shorthand in properties) {
if (style.hasOwnProperty(shorthand)) {
properties[shorthand].forEach((longhand) => {
if (style.hasOwnProperty(longhand)) {
console.warn(
"Found '" + shorthand + "' mixed with '" + longhand + "'!"
)
}
})
}
}
}
import { validateStyle } from './validateStyle'
const style = {
padding: 5,
paddingLeft: 10,
paddingRight: 10,
}
validateStyle(style)

This would log two warnings:

  • Found 'padding' mixed with 'paddingLeft'!
  • Found 'padding' mixed with 'paddingRight'!

Now we get automatic warnings whenever we accidently mix shorthand and longhand properties, but there're still two big issues with this solution:

  1. This technique is called runtime validation and happens just as we render the styles, so we might still miss edge cases which we haven't thought about in development. Additionally, since everything is done in the browser, this might lead to performance issues.
  2. We still have to put manual effort into fixing it. A way to solve that is by turning it into a linting rule with autofix capabilities, but due to the dynamic nature of JavaScript and the way we build and extend styles in modern applications, this is an incredibly difficult, if not impossible, task.

So, while this approach is a bit more safe than pure discipline, it also adds a lot of tradeoffs and still doesn't really solve the problem itself.

Expanding Shorthands

The next approach is an interesting one. Since we're in JavaScript-land, we can do all kind of object manipulation and processing to modify our style object. We can use that ability to "autofix" our style objects before they're actually rendered.

What if we could automatically expand shorthand value into their respective longhand values. For example:

const style = {
padding: '5px 10px 20px',
}

becomes

const style = {
paddingTop: 5,
paddingRight: 10,
paddingBottom: 20,
paddingLeft: 10,
}

It turns out that most properties are rather simple to convert. We crafted a package called inline-style-expand-shorthand(new tab) which aims to do that.

In Fela, we have this concept of plugins(new tab), that can be used for all kinds of style processing. We are now able to expose that package as a plugin and that's it. All of our shorthands are now automatically expanded to their longhand equivalents!

Sounds to good to be true, right? Right. As with everything, this approach also comes with some downsides:

  • While most shorthands are in fact easy to expand, the more complex ones aren't. It even gets more complicated if we want to merge with existing longhand properties.
  • In order to support all properties (and there're a lot of them), a lot of logic needs to be implemented. Not only does this increase the bundle size, but it also adds a big processing step to all renders which eventually impacts performance.

While this approach is the first actual solution to the problem, it also comes at a big cost. That's why I didn't stop here and tried to find an even better solution.

Ordering Rules

Now that we tried all kind of style manipulation, I thought about whether it's possible to achieve a deterministic result without actually changing the style object at all.
What if we could ensure that the order in which those Atomic CSS classes are rendered would always resolve in a deterministic way. So to speak, rendering all longhand properties after shorthand properties.

Most modern CSS-in-JS libraries use the insertRule(new tab) web API to insert styles into a stylesheet. Looking at the API documentation, we can see that insertRule actually takes a second parameter: index.
That means, in theory we can programmatically ensure that all classes are rendered in "correct" order, no matter when they're rendered.

Long story short: Don't try to do that. It is extremely slow to render rules in a "sorted" way. Especially with Atomic CSS where we have to render tons of rules initially. You have to keep track of every rule to calculate the exact index at which each rule has to be inserted.

While almost giving up, I came across another idea that might have no particular impact on neither performance nor bundle size.

Repeated Selectors

What if we could "force" our longhand rules to have higher specificity than the shorthand counterparts so that the cascade doesn't matter?

Turns out that we can do that by repeating selectors.
While I personally consider this a clever hack, it's actually part of the "Selectors Level 3" specification(new tab). That means that with the following CSS:

.a.a {
padding-left: 10px;
}
.b {
padding: 5px;
}

We would get a calculated padding value of 5px 5px 5px 10px even though the order is "incorrect"! All we have to do is to repeat the selector twice for every rule containing a longhand property.

In Fela, we now ship a propertyPriority configuration option(new tab), with which one can configure all properties that need to have repeated selectors. In addition to that, we also ship fela-enforce-longhands(new tab) which automatically applies an opinionated configuration setting. Just add it to your enhancers(new tab) list and you're done.

Conclusion

After a long ride with all sorts of different solution, we finally found a solution that, compared to the rest, is so simple and yet so powerful.
All we really needed was a built-in "feature" in native CSS rather than any complex JavaScript processing solution.

I hope this articles helps everyone to understand the origin of this cascade issue with generated Atomic CSS. If you haven't heard about Atomic CSS-in-JS libraries before, I can only welcome you to try any of the above mentioned libraries.

Credits

Thanks to my friend Kitty(new tab) for helping me with the inline-style-expand-shorthand(new tab) package and for reviewing this article.
Credits to Nicolas Gallagher(new tab) who first came up with the idea of repeated selectors for React Native Web(new tab).

Thanks for reading!

I’m Robin, Freelancer & Frontend Architect from Germany.
Any question regarding this article? Reach out to me on Twitter(new tab)!
You can also find me on GitHub(new tab).