If you're starting a web project, you're probably starting with a CSS reset, and for most of us, that means reaching for a trusted community solution - dropping it in and moving on. If you're building a design system, though, that habit may be working against you.
The existing solutions
The community reset ecosystem is genuinely good. Each tool approaches the browser compatibility problem from a slightly different angle. Some examples include:
- Eric Meyer's Reset is a classic: it zeros out margins, padding, and font sizes across every element, giving you a completely blank slate. It's minimal and predictable, which made it influential.
- Normalize.css smooths over inconsistencies while preserving the ones that are actually useful.
-
sanitize.css and modern-normalize continue that evolution - incorporating contemporary best practices like
box-sizing: border-box, improved form element handling, and accessibility-aware defaults.
The problem isn't that any of these are bad. The problem is that they're all deliberately, necessarily generic. They can't know anything about your typeface, your color palette, your spacing scale, or how your interactive elements should behave. That's by design - they're tools for everyone, which means they're perfectly tailored for no one.
The problem
If you're building a design system, generic is exactly what you don't want your reset to be.
The moment you drop in one of these resets and start building, you find yourself doing a second round of work. You apply your typeface to body. You reset margins on headings. You make form elements inherit fonts. You define focus styles. You're re-resetting - applying your design language on top of a layer that just cleared out the browser's defaults and replaced them with... more defaults you'll override.
Worse, that duplication doesn't stay in one place. Every component you build either re-declares these foundational styles or silently assumes they're already set upstream. You end up with either redundancy or invisible dependencies, and neither is a foundation you want to build a system on.
The solution
Your reset should be the first layer of your design system, not a prerequisite to it.
Rather than resetting to neutral and then applying your design language on top, encode your design language directly into the reset. Keep your tokens as the single source of truth - defined separately, compiled to CSS custom properties - and write your reset against those variables. Typography, color, spacing, focus styles, border radii: all derived from your token set, applied globally, from the very first stylesheet.
To see this in practice, consider a simple token set compiled to CSS custom properties:
:root {
--font-family-base: Inter, system-ui, sans-serif;
--font-size-base: 1rem;
--line-height-base: 1.5;
--line-height-heading: 1.2;
--color-text: #111827;
--color-background: #ffffff;
--color-interactive: #2563eb;
--color-focus-ring: rgba(37, 99, 235, 0.24);
--radius-base: 8px;
--stroke-sm: 2px;
}
A reset written against those tokens looks like this:
*, *::before, *::after { box-sizing: border-box; }
html {
font-family: var(--font-family-base);
font-size: var(--font-size-base);
-webkit-font-smoothing: antialiased;
}
body {
margin: 0;
color: var(--color-text);
background: var(--color-background);
line-height: var(--line-height-base);
}
h1, h2, h3, h4, h5, h6 { margin: 0; font-weight: 600; line-height: var(--line-height-heading); }
a { color: var(--color-interactive); text-decoration: none; }
button, input, select, textarea { font: inherit; color: inherit; border-radius: var(--radius-base); }
:focus { outline: none; }
:focus-visible { outline: solid var(--stroke-sm) var(--color-focus-ring); outline-offset: 2px; }
Every value comes from a token. Change a token and the whole page updates - no hunting through component stylesheets. And because the reset applies these globally, components inherit font, color, and focus behavior without having to declare any of it themselves.
The projects that figured this out early are some of the most widely adopted UI systems out there.
- Bootstrap's Reboot layers Bootstrap's own typographic scale and element defaults directly into the reset so every component sits on Bootstrap-flavored ground, not neutral ground.
- Tailwind's Preflight bakes Tailwind-specific expectations - unstyled headings, consistent box-sizing, no default margins - into what looks like a reset but is really the system's global baseline.
- MUI's CssBaseline injects Material Design's global defaults as part of the component tree itself, with font family and background color driven by MUI's theme.
- Shopify Polaris, IBM Carbon, and Adobe Spectrum all follow the same principle: their base styles are an expression of the system, not a blank slate that the system patches over.
The practical result is that components become minimal. A button doesn't need to declare its font, its border-radius, or its focus style - the reset already sets those from your tokens. A heading doesn't need a margin reset. An input doesn't need font: inherit. System-level concerns live at the system level, and component CSS narrows to just what makes that component distinct. Drop the reset into a new project, and it immediately looks and feels like your system, before a single component has been built.
You also don't need to layer a community reset underneath. Your system reset already handles cross-browser normalization for the elements it touches - it just does so with your values rather than neutral placeholders. Adding Normalize on top reintroduces defaults you'd only have to undo.
Pairing with CSS cascade layers
CSS cascade layers (@layer) are a natural complement to this approach. Layers let you declare an explicit priority order for your stylesheets, so component styles always win over reset styles without needing higher specificity. That's important: it means your reset can be as thorough as it needs to be without creating a specificity arms race inside components.
A layer order for a design system could look like this:
@layer reset, tokens, components, utilities;
Tokens live outside layers entirely - since CSS custom properties don't participate in the cascade the same way, they're always available regardless of layer order. Your reset goes in the reset layer, which sits at the bottom of the stack. Anything in components or utilities automatically wins without any extra specificity tricks.
In practice:
/* tokens.css -- no layer, always available */
:root {
--font-family-base: Inter, system-ui, sans-serif;
--color-text: #111827;
--color-interactive: #2563eb;
--radius-base: 8px;
}
/* reset.css */
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
font-family: var(--font-family-base);
color: var(--color-text);
}
a { color: var(--color-interactive); }
button, input, select, textarea { font: inherit; border-radius: var(--radius-base); }
}
/* button.css */
@layer components {
.btn {
/* no need to re-declare font, color, or border-radius */
padding: 0.5rem 1rem;
background: var(--color-interactive);
color: #fff;
}
}
With layers in place, the reset handles the system defaults and gets out of the way. Components sit cleanly above it in the cascade, and you never have to inflate specificity just to override a reset rule. It also makes the architecture easier to communicate to contributors - the layer declaration at the top of your stylesheet is a readable map of how the system is organized.
Wrap up
Your CSS reset is the first thing the browser renders. It's the foundation every component sits on. The community resets earned their place by solving a real problem, but their neutrality is a constraint you don't need if you're building a design system. Make your reset the first implementation of your design language - opinionated, token-driven, and already doing work before a single component loads.
Top comments (0)