Jul 13, 2025

There’s no such thing as a CSS reset

CSS resets are a technique nearly as long lived as the modern CSS era itself, stretching all the way from the CSS Zen Garden days to inclusion in mass-market current-day tools like Tailwind. Resets are a staple in the CSS toolkit.

But here’s the thing; there’s no such thing as a CSS reset. Now, to be clear, I’m not suggesting some kind of long-running mass hallucination. I’m suggesting CSS resets don’t do what the name says. The word “reset” implies an objective default state that you’re restoring to, but the only objective default state is what browsers ship. You cannot “reset” browser styles by addition. Resets are inherently subjective, which means in practice, you are not so much resetting as you are defining default element styles for a website.

You might think I’m nit-picking semantics – it’s true, I am – but it’s a useful semantic distinction. Because when it comes to defining default styles you only have two choices:

  1. Define defaults that are useful without additional styling.
  2. Define defaults that are not useful without additional styling.

Using those same two bookends of CSS Zen Garden and Tailwind, we started with approach #1, and over time went hard for approach #2. Like, “men would rather invent a CSS framework rather than style a single <button> tag” hard.

To be fair, there were reasons.

Mo’ CSS, mo’ problems

At the risk of flattening nuance, you can more or less summarize the the shift from “embrace the cascade” to “burn the cascade with fire” in two words; specificity woes.

Using tag selectors exclusively inherently lead to deeply nested selectors, which to say another way, increasingly specific selectors. Inevitably you’d run into a situation where you want a style to override one of those highly specific selectors, and you only had a few options:

  1. Add more selector depth to further increase specificity.
  2. Play with source order.
  3. Use the nuclear option, AKA !important.

These tools were fine-ish at small scales and in small doses, but big projects would quickly spiral out of control. The more CSS you added, the more painful it got.

Which is why when strategies like BEM emerged, they felt like a breath of fresh air. You could just… not think about specificity and the cascade at all. Everything was flat, tightly encapsulated, and highly predictable, with nary an !important in sight.

That idea of highly encapsulated component styles became the more or less defacto approach across a variety of permutations; Tailwind, styled components, Atomic CSS, shadow DOM, etc. But through all of those permutations, the humble CSS reset lived on as an element style vestigial tail. It would after all be pretty obnoxious to have to zero out the margin, remove list bullets, etc. in every component in a project where that’s not what you want 90% of the time.

Why use element styles?

The status quo became the status quo for valid reasons, but that’s not to say there weren’t any tradeoffs made along the way. There are some wonderful benefits to directly usable styled elements, and new tools like cascasde layers, and :where() mitigate so many of the cons that the pros are worth a second look. The top 3, in my humble opinion:

  1. Free names: Word on the street is that naming things is hard. What if I told you that there are 100+ names that you can use right now, today, for the low, low price of “free”? Think of elements like components, but ones that come packed in the browser. Custom elements, without the “custom” part. You can just like, use them.
  2. Enforce semantics: People (rightly) bemoan the fact that so many websites composed of undifferentiated div soup, but that’s large in part due to the fact that most modern approaches to CSS intentionally decouple style from structure. By making elements functional components, you can strongly encourage use of semantic HTML. Out with <div class="button">, in with <button>.
  3. Simple page composition: With styled elements, you can just write simple markup or Markdown to scaffold a page. Maybe toss a class in here or there when there’s an exception. I’ve built a bunch of marketing sites over the years and it’s extremely pleasant to design/build a page by starting with semantic markup and having 50%-60% of the work done.

Putting the baby back in the bathwater

Cascade layers are incredible. I stan cascade layers. Every word of this post to this point has been a tee up for me to sing the praises of cascade layers. I am convinced that if cascade layers had existed from the jump, we would have an order of magnitude fewer “CSS hard” memes. They’re easy to use, but give you precise control over CSS specificity (which is to say, they’re very powerful). Here’s how I like to use them:

@layer base, components, utilities;
@import "_base/_base.css" layer(base);
@import "_components/_components.css" layer(components);
@import "_utilities/_utilities.css" layer(utilities);

Each of those imports are an entrypoint that in turn imports more granular CSS files. You can use @layer rules too, but I prefer this approach since it means you can ignore cascade layers outside of this basic config. With 4 lines of CSS, you can more or less run wild with useful, directly usable element styles without them ever overriding your component and utility styles. This is a pretty simple example (I like to keep things simple), but you can use more layers, add sub-layers, whatever makes sense for the project at hand.

Note that I said “more or less run wild” above. Unless you’re already using a defensive approach to writing component CSS, it’s still true that it’d often be impractical or annoying to have to zero out margins, unset pseudo elements, etc. from base styles in all of your component definitions.

A great way to solve that problem is by making spicy element styles opt-in, like so:

:where(article) {
  & > * + * {
    margin-block-start: var(--flow-space);
  }

  & li::before {
    content: " ";
    display: block;
    height: 0.1rem;
    width: 100%;
  }
}

Your core element styles take care of the 80% case, opt-in element styles take care of the 20% case, and you can keep writing component CSS as if you’re using a reset + pure encapsulation approach.

With cascade layers and opt-in spicy styles, you might still have a few instances where you need to manage specificity within layers. A sub-layer might be a good choice, but I rely on :where() selectors primarily, which let you use nested selectors without increasing specificity. Combining cascade layers with :where() lets you create some truly wimpy styles:

@layer base {
  :where(ul > li) {
    color: red;
  }
}

Everything beats this style. You look at this style wrong and it crumbles. This is the fainting goat of CSS.

There are more techniques and more tools in the tool belt when it comes to managing the cascade (looking at you @scope), but the point is, CSS is in a fundamentally different place than it was even 5 years ago, and it’s worth taking the time to reconsider decisions made around outdated tradeoffs.

Read more notes