Why the Hardest Part of CSS Is Specificity

It’s not easy to engineer unbreakable CSS with no side effects

Joe Honton
Nov 14, 2019 · 7 min read
It feels like the “cascading” part of CSS is for “cascading ripple effect”
It feels like the “cascading” part of CSS is for “cascading ripple effect”

It was Tech Tuesday at Tangled Web Services and no one had anything exciting to share. Devin and Ken’s performance benchmarking had been put to bed. Ernesto’s excitement over speculative push had died down. And bug triage and JIRA grooming was looming. Everyone was in a grumpy mood.

And it was still only Tuesday.

Even Clarissa, who was always the most sanguine of the crew, was noticeably on edge. But since no one else was volunteering to share their tech news, Clarissa decided to fill the void.

“I’ve come to the conclusion that the hardest part of CSS is specificity,” she began. “Not naming conventions, not modularity, but specificity.”

Devin and Ken, die-hard backend engineers, wanted to roll their eyes. What’s so hard about CSS they were thinking. . . . Those front-end guys don’t know what real problems look like. . . . CSS is for people who can’t write algorithms. (Of course, we can’t know for sure what they were thinking, but we can guess that it was something along these lines.)

“By specificity,” Clarissa explained, “I mean the algorithm that the browser follows to determine the hierarchical cascade of selectors that apply to each element of the page.”

The guys started fidgeting in silent shame at their unkind thoughts, as she matter-of-factly laid out the problem in engineering jargon, but with impressive clarity.

Clarissa leaned into it. Her explanation went something like this —

CSS allows three types of things to be used in rules: tag types, class names, and identifiers. So each element of the page can be the target of multiple selectors at the same time. On top of this, CSS also allows individual elements to declare their own rules — with the style attribute — to override the selector-based rules.

CSS rules may be simple, such as declaring the paragraph tag type to have a default padding. Or they may be complex, such as declaring that all paragraphs, except the first one after a heading, should be indented.

But this is misleading because specificity is not about simplicity versus complexity. Rather, it’s about the degree of importance. And the importance is determined mathematically by counts and weights. That is, how many names are used to declare the selector, and what is the weight of each name.

Names are weighted like this:

selector     |  weight  |  examples
------------ | -------- | -----------------------------
tag type | 1 | p span div
classname | 10 | .intro .first .primary
pseudo-class | 10 | :first-of-type :last-child
identifier | 100 | #err-msg #story #figure1
style | 1000 | <p style="text-indent:1em">

The formula for specificity (Sp) is straightforward:

Sp = Tn + (Cn * 10) + (Pn * 10) + (In * 100) + (S * 1000)

where Tn is the number of tag types in the selector, Cn is the number of class names in the selector, Pn is the number of pseudo-classes in the selector, In is the number of identifiers in the selector, and S is 1 when an element has a style attribute and 0 when it doesn't.

For example, consider these four HTML elements:

<p>Only Australia and Antarctica are true continents.

They can each be targeted with simple selectors, but their specificities are widely different.

p       { text-indent:0em } /* Sp = 1 */
.suez { text-indent:0em } /* Sp = 10 */
#panama { text-indent:0em } /* Sp = 100 */

The fourth paragraph is styled directly on the element, so it has an Sp of 1000. This is the sledgehammer approach to styling, and it always wins, because no matter how many class names or identifiers are part of the selector, they will (practically) never add up to be more than 1000.

And the exception to all these is the !important keyword, which can be added to an attribute's declaration, where it will effectively boost that part of the selector by 10,000 — telling the browser to ignore everything else (even the element's style attribute) and just do it!

Now to explain the situation with more complexity, suppose the goal is to force the first paragraph to be in classic book style, without an indent, but to indent all of the following paragraphs.

p + p { text-indent:1em } /* Sp = 2 */

This new selector will correctly target only paragraphs that come immediately after a preceding paragraph, thus fulfilling our goal.

But unfortunately, for our continent example above, this new selector will never be applied because its specificity (2) is lower than the specificity of the selectors on paragraphs 2, 3 and 4 (Sp = 10, Sp = 100 and Sp = 1000).

Solutions to the problem abound. Adding a noindent class name or identifier to the first paragraph and selecting it that way —

p           { text-indent:1em } /* Sp = 1 */
p.noindent { text-indent:0em } /* Sp = 11 */

Or wrapping the block of paragraphs with a new element and targeting the wrapper —

p                        { text-indent:1em } /* Sp = 1 */
.wrapper p:first-of-type { text-indent:0em } /* Sp = 21 */

Or removing the text-indent attribute from .suez and #panama and simplifying the rules to be just —

p     { text-indent:0em } /* Sp = 1 */
p + p { text-indent:1em } /* Sp = 2 */

This last one just feels right. The selectors are generic enough to apply everywhere. And there aren’t any class names to be sprinkled into the HTML, so there’s nothing new to proofread. Plus there are no extra non-semantic wrappers bloating the document. It’s just styling. True separation of concerns.

But unfortunately, there were other places on the page where this new rule wasn’t being applied. For example, sections were used to divide the page into manageable pieces, and these did not get the benefit of the new p + p text-indent rule:

section p       { font-weight:300; text-indent:0em } /* Sp = 2 */
section.intro p { font-weight:400; text-indent:0em } /* Sp = 12 */
section#quote p { font-weight:600; text-indent:0em } /* Sp = 102 */

And worse, there were places on the page where this new rule was being applied when it shouldn’t be. There were paragraphs within asides that were explicitly designed without indentation, but the new rule was applying indents anyway —

<aside>
<p>Antarctica is a true continent, even though we can't see it.
<p>The Arctic is not — it's nothing but frozen water up there.
</aside>

These types of problems could have been avoided if the CSS rules were designed and applied before the content was solidified. But new content challenges and new styling needs come hand-in-hand as the story develops. CSS is always iterative.

Yes, for the most part, adding new rules can be done without too much disruption. But only if you thoroughly proof-edit everything afterward. And there’s the rub.

Unlike other coding practices where you can create test suites, and automate the regression testing process, CSS testing cannot be automated to find all those unwanted side effects. How do you test for a 1px shift that causes text to wrap inside a button? How do you test for two adjacent elements collapsing their top and bottom margins in unanticipated ways? And how do you test for unwanted underscores when some anchors have text-decoration and others have border-bottoms?

On top of this, CSS changes can be global. For example, changes in tag types are, by definition, not local. Like ripples in the pond, such changes can disturb far away islands in their wake. Other parts of your document. And other documents.

And even worse than adding something new to your stylesheet is trying to make changes. As soon as you try to refactor an existing stylesheet you end up belly-flopping in the water. Splash! Ripples!

Trying to get rid of the cruft that’s built up is a thankless chore. Trying to clean up code to make it orthogonal and tight, is just asking for trouble.

“I’ve come to the conclusion,” Clarissa wrapped up her rant, “that the cascading part of CSS, means cascading ripple effect — every time you change something, you disturb something else.”

The room fell silent for a moment.

“I feel your pain,” Devin offered.

“But wait,” asked Ken, “can’t you use the browser inspector to help in some way?”

“Yes, but only to a certain extent,” Clarissa answered, without enthusiasm. “The browser inspector lets you poke at a single element and see all the rules and overrides that are assembled into attribute values. But it doesn’t have any capability to look at a style sheet as a whole, and see what elements are affected by each declaration.

“I guess what I need is a tool to allow me to pick a target selector and to see all of the related selectors, ordered by specificity. Which ones have a lower specificity and are going to be clobbered. And which ones have higher specificity and are going to block my target.

“So if any of you are looking for a cool project for your ‘20% time’, this would be a good one.”

There were no immediate takers on Clarissa’s idea for a specificity visualizer.

Clarissa didn’t really expect her guys to pick up on the idea, but she was glad to have planted the seed. Maybe it would germinate somewhere else.

Image for post
Image for post

No minifig characters were harmed in the production of this Tangled Web Services episode.

The Startup

Medium's largest active publication, followed by +721K people. Follow to join our community.

Joe Honton

Written by

Using distraction-free tools for better reading, writing and publishing, and loving it!

The Startup

Medium's largest active publication, followed by +721K people. Follow to join our community.

Joe Honton

Written by

Using distraction-free tools for better reading, writing and publishing, and loving it!

The Startup

Medium's largest active publication, followed by +721K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store