CSS

CSS Mysteries Explained: Specificity, Stacking, and Why Your Styles Don’t Apply

By Ken Key Aug 9, 2025 4 min read Updated Oct 16, 2026

CSS Mysteries Explained: Specificity, Stacking, and Why Your Styles Don’t Apply
The short answer

Your styles aren't applying because another rule won the cascade. Specificity scores (ID, class, type) aren't base-10, so one ID beats any number of classes. Inline styles beat both. Use @layer to control order explicitly, and keep selectors flat instead of stacking classes to fight back.

Part of the complete web design guide.

On this page

Most CSS bugs aren’t bugs. They’re the cascade doing exactly what it was told, and you not knowing which rule won. I’ve spent years debugging other people’s stylesheets, and the same four or five mysteries come up over and over: specificity wars, stacking contexts, margin collapse, and flexbox sizing that refuses to shrink. Here’s what’s actually happening in each case.

Specificity is a scoring system, not a suggestion

When two rules target the same element and set the same property, the browser picks a winner by specificity. It’s a three-part score, written as (ID, class, type). Inline styles sit outside that score entirely and beat everything except !important.

/* (0,1,0) - one class */
.card { color: black; }

/* (0,2,0) - two classes, wins */
.card.featured { color: red; }

/* (1,0,1) - one ID, one type. Beats both above. */
#sidebar p { color: blue; }

The gotcha is that the score is not base-10 math. A thousand classes still lose to one ID. People write .a.b.c.d.e.f.g.h.i.j.k thinking they’re climbing the ladder, and a single #main rule sitting 400 lines earlier still wins.

The fix isn’t more specificity

Adding weight to fight weight is how stylesheets rot. Two better moves:

  • Use @layer to control the cascade explicitly instead of gaming the score. Layer order beats specificity: any rule in a later layer wins over an earlier layer, regardless of selectors.
  • Keep selectors flat. One class per element is enough 95% of the time. If you need two, ask why the component isn’t encapsulated.
@layer reset, base, components, utilities;

@layer base {
 a { color: navy; } /* loses to components */
}

@layer components {
 .button { color: white; } /* wins, no specificity fight */
}

Layers are supported everywhere that matters now, and they solve the problem at the source instead of papering over it.

Why z-index “doesn’t work”

This one trips up almost everyone. z-index: 9999 does nothing if the element is inside a stacking context that sits below a sibling’s stacking context. You’re not competing against the element you think you’re competing against. You’re competing against its ancestor’s stacking context.

An element creates a new stacking context when it has, among other things:

  • position other than static plus a z-index that isn’t auto
  • opacity less than 1
  • transform, filter, perspective, or will-change set to anything
  • position: fixed or sticky
  • isolation: isolate (the honest way to do it on purpose)

So a modal with z-index: 9999 can still render behind a header with z-index: 10 if the modal lives inside a card that has transform: translateY(0) on it. The card created a context, and everything inside it is trapped there.

How to debug it fast

Open devtools, select the element, and walk up the DOM. At each ancestor, check the computed transform, opacity, filter, and z-index. The first ancestor that creates a stacking context is your ceiling. Fix the ceiling, not the child. Nine times out of ten the culprit is an animation library that left a transform on a wrapper.

Margin collapse, the rule nobody told you about

Adjacent vertical margins collapse. The larger one wins, they don’t add. A paragraph with margin-bottom: 30px followed by one with margin-top: 20px produces 30px of space, not 50px.

It gets weirder. A parent’s top margin collapses with its first child’s top margin if there’s no border, padding, or content between them. That’s why your heading pushes the whole container down the page instead of sitting inside it.

How to stop it

  • Switch the container to display: flex or grid. Flex and grid items don’t collapse margins at all. This is the cleanest fix and probably why your layout behaves once you add display: flex.
  • Add overflow: hidden or padding-top: 1px to the parent. It works, but it’s a hack.
  • Use a single-direction margin system. Set only margin-bottom (or use gap) and stop thinking about collapse entirely.

Why flex items won’t shrink below their content

You put two items in a flex row, one has a long string of text, and it blows past the container instead of shrinking. The default min-width of a flex item is auto, which means “never shrink smaller than my content.” Text and images set that floor.

.row {
 display: flex;
 gap: 1rem;
}

.row > * {
 min-width: 0; /* let it actually shrink */
}

That one line fixes more layout bugs than any framework. Same idea applies to grid: min-width: 0 on grid items, or use minmax(0, 1fr) in your track definition.

Where this actually costs you money

If you’ve ever paid someone to fix a “broken layout” that turned out to be a stacking context, you know these aren’t academic. On client work, whether it’s a small business site or something bigger, the difference between a stylesheet that fights back and one that cooperates is hours of billed time. If you’re weighing whether to hand off a messy front end or rebuild it clean, that’s the kind of thing worth talking through with a Long Island web design partner who’ll actually read the CSS instead of slapping !important on it.

The short version

Specificity is a fixed scoring system, so stop trying to out-score it and use layers. Z-index only competes within a stacking context, so find the ceiling. Margins collapse vertically unless you’re in flex or grid. Flex items won’t shrink below content unless you set min-width: 0. Learn these four and most of the “mystery” goes away.

FAQ

Does !important override everything?

Almost. It beats normal declarations everywhere, but another !important with higher specificity still wins, and layer order flips for important rules. Treat it as a last resort, not a tool.

Why does my z-index work in one place and not another?

Because the element is inside a different stacking context. Walk up the DOM and find the first ancestor with a transform, opacity below 1, or a positioned z-index. That ancestor is the real boundary.

Should I still use floats?

Only for wrapping text around an image. For layout, flexbox and grid replaced them years ago and don’t have the collapse and clearing problems.

Have a project like this?

I do Long Island web design for Long Island businesses. If you need this done right, let’s talk.