Why Your CSS Selectors Keep Breaking

I spent three hours last Tuesday tracking down a bug where a button wouldn’t turn blue. Three hours. The culprit? A rogue #app-container .sidebar nav ul li button.primary selector written by a contractor four years ago.

Well, that’s not entirely accurate — this is exactly the kind of thing that makes developers hate CSS.

We treat selectors like simple target practice. Just keep adding classes until the style applies, right? Throw an !important on there if you’re feeling desperate. But that’s how codebases rot. Writing CSS isn’t just about targeting an element so you can change its background color. It’s about managing a global state machine without breaking everything else.

The Specificity Trap (and the escape hatch)

I used to write incredibly defensive CSS. Everything was heavily nested. I thought I was protecting my components from the rest of the app.

And then I actually understood how :where() works.

If you aren’t using this yet, you’re working too hard. The :where() pseudo-class applies a style but keeps the specificity of the entire selector at zero. It’s essentially a free pass to write base styles that are effortlessly overwritten later.According to the CSS Selectors Level 4 specification, “:where() represents a zero-specificity pseudo-class, which means its specificity doesn’t contribute to the overall specificity calculation.”

I was migrating a legacy React 18.2.0 codebase last month. We had this massive, fragile typography stylesheet where headings were constantly fighting each other based on where they lived in the DOM. By wrapping our base resets in :where(), we stripped out hundreds of forced overrides. It dropped our compiled CSS bundle from 94KB to about 42KB gzipped.

CSS code on computer screen - A computer screen displays code on a desktop.
CSS code on computer screen – A computer screen displays code on a desktop.

More importantly? I stopped having to fight my own defaults. When a component needed a specific style, a single simple class was enough to win the specificity war.

The dark side of :has()

Let’s talk about the parent selector we waited two decades for.

We finally have :has(). So if you’re still writing JavaScript to toggle a has-active-child class on a wrapper div, please stop. The browser can do this natively now.

But here is my massive warning label for you.

I locked up a staging environment completely a few weeks ago. I wrote what I thought was a clever little selector: main:has(.deeply-nested-error-state) .header-warning.

Sounds elegant. Except our DOM had about 8,000 nodes on that specific dashboard view. Because of how I wrote the selector, the browser had to re-evaluate the entire massive tree every single time a user typed a character into a search input. The paint flashing in Chrome DevTools looked like a strobe light.

The fix was painfully obvious in hindsight. Scope it tightly. Don’t use :has() on the body or main tags if you can avoid it. Bind it to the closest parent possible. I changed it to .dashboard-widget:has(.error-state) and the rendering bottleneck vanished instantly.Mozilla Developer Network’s documentation on the :has() selector cautions that it can be computationally expensive if not used carefully.

Attribute Selectors Are Dangerously Underused

CSS code on computer screen - Code on a computer screen
CSS code on computer screen – Code on a computer screen

Most people stop at [type="text"] and never look at attribute selectors again.

You can do so much weird, powerful stuff with them. I obsessively use them for debugging now. Drop this snippet into your dev environment right now:

[class*="undefined"] { border: 5px solid red !important; }

Watch how fast you catch React hydration errors or missing prop bugs. It saved me from pushing a broken layout to production twice this week alone.

You can also use them for state management without touching class names. [aria-expanded="true"] is way more accessible and cleaner than toggling an .is-open class. It forces you to write accessible HTML because your CSS literally won’t work if you don’t add the proper ARIA tags. It’s a great way to force yourself into good habits.Mozilla Developer Network’s guide on attribute selectors covers the various ways they can be used.

CSS code on computer screen - Code is displayed on a computer monitor.
CSS code on computer screen – Code is displayed on a computer monitor.

How I actually write this stuff now

I don’t guess how specificity works anymore. My brain simply can’t hold the mental model of a 10,000-line stylesheet.

And when things get weird, I isolate. I rip the HTML out, throw it into an isolated code playground, and strip the selectors down to their absolute minimum. If a selector has more than three parts (like .card > .content h2), I rewrite it. Flat is better than nested. Always.

I expect we’ll see native CSS mixins mature enough for production use by Q3 2027, which might completely change how we architect component styles again. But until then, keep your selectors flat, use :where() for your defaults, and respect the browser’s rendering engine.

But seriously, go add that undefined class trick to your dev build. You’ll thank me later.

Your email address will not be published. Required fields are marked *

Zeen Social Icons