loke.dev
Header image for Specificity Is Optional

Specificity Is Optional

Stop fighting the hierarchy of selectors and start orchestrating your styles by intent rather than selector weight.

· 4 min read

Specificity Is Optional

I used to spend way too much time playing "Selector Tetris." I’d write a perfectly logical CSS class, refresh the browser, and... nothing. I’d open the dev tools only to find that some hyper-specific selector from three months ago—something like .sidebar .nav-item .link.active—was outmuscling my new styles because it had one extra class name in the chain. My shameful solution? I’d slap an !important on the property and move on, feeling like a failure. We’ve all been in that arms race, but here’s the secret: specificity is only a problem if you let it be the boss.

The Weight of Our Mistakes

In the old days (meaning, like, three years ago), CSS was a battle of weights. You had IDs, classes, and elements. If you wanted to override a style, you either had to match the selector exactly or go one step further. It led to code that looked like this:

/* Why did I do this to myself? */
body main .content-wrapper article.post .entry-content p {
  color: #333;
}

This is brittle. If you decide to change that article to a section, your styles break. If you want to change the paragraph color for a specific "dark mode" component, you have to write a selector even longer than the one above. It’s a nightmare to maintain because you're fighting the weight of the selector rather than the intent of the design.

Enter the Cascade Layer

The game changed with @layer. If you haven't started using CSS Layers yet, you're missing out on the best way to tell specificity to sit down and shut up.

With layers, the order of the layers matters more than the weight of the selectors inside them. You can have a single class in a "high" layer override a ten-level-deep ID selector in a "low" layer.

Here is how you set the hierarchy:

@layer reset, base, components, utilities;

@layer base {
  /* This has high specificity, but it's in a low layer */
  #main-content h1.title {
    color: black;
    margin-bottom: 2rem;
  }
}

@layer components {
  /* This is just a simple class, but it wins because 
     the 'components' layer comes after 'base' */
  .card-title {
    color: blue;
  }
}

Even though #main-content h1.title is technically "heavier," the .card-title wins because it lives in the components layer, which we defined as being more important than the base layer.

Organizing by Intent

The beauty of this is that you can stop worrying about how "strong" your selector is and start thinking about what the CSS is actually *doing*.

I usually break things down into four buckets:

1. Reset: Boring stuff like box-sizing and removing margins.
2. Base: Typography, default link colors, the "global" look.
3. Components: Buttons, cards, navbars—the meat of the UI.
4. Utilities: The "I need this to be red right now" classes.

By defining your layers at the very top of your CSS file, you lock in the priority:

@layer reset, theme, components, utilities;

@layer utilities {
  .text-error {
    color: #d32f2f !important; /* Yes, !important still has a place here! */
  }
}

The "Gotcha" (Because there's always one)

The most important thing to remember is that unlayered styles are the strongest.

If you write a style outside of any @layer block, it acts like it’s in the highest possible layer. This tripped me up for a week. I thought my layers were broken, but really, I just had some old "loose" CSS floating around that was stomping all over my carefully organized layers.

Also, the order of your @layer definition at the top of the file is everything.

/* utilities wins, reset loses */
@layer reset, components, utilities; 

If you accidentally swap them, your reset might override your components, and you’ll find yourself right back in the Inspector, questioning your life choices.

Stop the Chaining

The next time you find yourself writing .container .row .col .my-button, stop. Ask yourself if you’re trying to beat a specificity score or if you’re just trying to style a button.

Wrap your base styles in a @layer base, put your component styles in @layer components, and suddenly, a single class name is all you’ll ever need. Specificity becomes a choice, not a constraint. And honestly? My "delete" key has never felt more satisfying than when I'm ripping out 5-level deep nested selectors.