Strategies for Keeping CSS Specificity Low

Avatar of Chris Coyier
Chris Coyier on (Updated on )

Keeping CSS specificity low across all the selectors in your project is a worthy goal. It’s generally a sign that things are in relative harmony. You aren’t fighting against yourself and you have plenty of room to override styles when you need to. Specificity on selectors tends to creep up over time, and there is a hard ceiling to that. I’m sure we’ve all felt the pain of !important tags and inline styles.

So how do we keep that specificity low over time?

Give yourself the class you need

Perhaps you’re writing a high-specificity selector because you’re overriding an already-existing selector. Maybe the element you’re trying to select is included on multiple pages, so you select it from a higher-up class:

.section-header {
  /* normal styles */
}

body.about-page .section-header {
  /* override with higher specificity */
}

However you feel about this, recognize that that specificity is creeping up here. To prevent this, is there a way you can alter the class name on that element you’re trying to style directly instead? Sometimes creating some server side helper functions or variables for emitting classes can be helpful, to avoid logic in views.

<header class="<%= header_class %>">
  Which could output one class, or both, as desired.
</header>
.section-header {
  /* normal styles */
}

.about-section-header {
  /* override with same specificity */
  /* possibly extend the standard class */
}

Consider the source-order of the stylesheets

Along that same trying-to-override-something vein, perhaps you are applying an additional class name to handle a style override for you.

<header class="section-header section-header-about">
  
</header>

But where you are writing your override styles with .section-header-about are actually getting overridden by the existing class. That can happen because of selector order in the stylesheet. Both selectors have the exact same specificity, so the rules from whichever one is declared last win.

Fixing that means just ensuring that where ever you write your override class comes later. Perhaps organize your stylesheets something like:

@import "third-party-and-libs-and-stuff";

@import "global-stuff";

@import "section-stuff";

@import "specific-things-and-potential-overrides";

Reduce the specificity of the thing you’re overriding

You know what they say about leaving things better than you found them. You should do that.

If there is an ID on an element and that’s what it’s being styled by, can you remove it? Definitely do a project-wide search for #that before you do it. It might be being used as a JS hook (perfectly fine) in which case you should leave it alone and either add a class or use a class already on it for the CSS.

Don’t avoid classes to begin with

I’ve been known to use a selector like:

.module > h2 {

}

That’ll work fine, until they day you want a special style for that h2 element. You might go in your your markup and be like:

<div class="module">
  <h2 class="unique">
    Special Header
  </h2>
</div>

But sadly:

.module > h2 {
  /* normal styles */
}
.unique {
  /* I'm going to lose this specificity battle */
}
.module .unique {
  /* This will work, but specificity creep! */
}

The specificity creep is happening because the original selector is biting us. That’s why almost all CSS methologies recommend flat structures in the vein of:

<div class="module">
  <h2 class="module-header">
  </h2>
  <div class="module-content">
  </div>
</div>

It can feel like more tedious work up-front because you might not need those classes right away. But by not avoiding that work (don’t be lazy!), the hooks you’ll have later can save your grief.

Use the cascade

As a project ages, it becomes more and more dangerous to alter selectors with low specificity, because they potentially can affect more things and have unintended consequences.

#im #just .gonna[do] .this .to .be #safe {
  /* cries (ಥ_ʖಥ) */
}

But affecting more things is the power of CSS. Having a solid base you’re building from hopefully means less overrides are ever necessary. The strategies for this can be things like…

Use a pattern library and/or style guide and/or atomic design

A pattern library (something like this) can mean you have what you are looking for when you need it. Need some tabs? Here you go, this is the established way for doing that. And it’s likely built in such a way the specificity is already light, so overriding won’t be overly difficult.

Atomic design (book) can guide how your site (or the pattern library) is built, so even if you don’t have a full pattern for what you need, you have the building blocks below it.

A style guide might save you, because it might enforce specific rules about specificity, in an attempt to save you from yourself.

Consider opt-in typography

At the same time you’re trying to use the cascade and have smart defaults for lots of elements, you might want to scope some of those smart defaults sometimes. For instance, a list element is often used for navigation and within content. The styles for them will be drastically different. So why not start with a clean slate and apply text styling only when needed.

/* reset styles globally */
ul, ol, li {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* scope text styles to text content */
.text-content {
  h1, h2, h3 {
  }
  p {
  }
  ul {
  }
  ol {
  }
  /* etc */
}

That does increase the specificity of those text selectors, but it means that rules specifically for text aren’t affecting more than they need to be and there is less need for overrides.

Outside of your control issues

Perhaps some third party code expects or injects certain HTML onto your page. You have to work with that. You can still try and use as low specificity selectors as you can. You can leave comments in the code indicating why the selectors are like this. You can use low specificity selectors, but use !important overrides. You can ask the people responsible for the non-ideal code if they can fix it.

Only up the specificity lightly, and note it

If you need to get into a specificity fight, rather than reaching for a sledgehammer like an ID or !important, trying something lighter.

A tag-qualifier is the minimum possible specificity upgrade.

ul.override {
  /* I win */  
}
.normal {
}

But limiting a selector to a specific tag is a weird limiting factor. Might be smart to just add an additional class instead. If another name doesn’t make sense, you can even use the same class if needed.

.nav.override {
}
.override.override {
}
.nav {
}

Just because nesting is nice, it doesn’t mean specificity has to go up

Nesting in preprocessors is sometimes discouraged because it makes it so easy to write overly selectors. But nesting is sometimes just visually nice in the stylesheet, because it groups things together making it easier to read and digest.

Bohdan Kokotko reminded me recently the ampersand in Sass can be used to essentially do namespacing rather than compound selectors.

.what {
    color: red;
   &-so {
      color: green;
      &-ever {
        color: blue;
      }
   }
}
.what {
  color: red;
}
.what-so {
  color: green;
}
.what-so-ever {
  color: blue;
}

The single-class wrapper

A rather forceful way to handle an override is use an existing wrapping element over the area you need to apply style overrides to, or add a new one.

<div class="override">

   ... existing stuff ...

</div>

You now have the ability to override any existing style in there by only adding a single class to the selector. It’s specificity creep, but an attempt at keeping it low.

.override .existing {
}

Only once

If you’re ever overriding an override, that’s a good place to stop and re-consider.

Can you give yourself just a single class that you can use instead? A single override can actually be efficient, like a variation on a pattern instead of creating a new pattern. An override of an override is a recipe for confusion and further fighting.

Just do better next time

If you’ve been bitten by specificity problems, even if fixing them is impractical right now, at least you know now that it’s problematic and can approach things differently next time.