Writing a Minimal Modern CSS Reset: Every Line Justified
CSS resets were born when browsers disagreed about basic defaults: margins on body, the size of h1 inside article, the display type of HTML5 elements. Those disagreements are largely gone. What is left is a smaller job — setting a few defaults that make modern layout predictable — and a reset that still carries a decade of normalisation rules is mostly dead weight that nobody on the team can explain. The narrow problem this page solves is writing a reset of around twenty rules where each line has a reason you could defend in code review, organised in a cascade layer so it never fights component styles. It belongs to Modern CSS Reset Strategies in the Mastering Container Queries & Responsive Layouts guide.
Why smaller is better
Every rule in a reset applies to every page, and every rule is a default that component authors must know about. A reset that sets line-height: 1 on headings, strips list styles from all lists and removes button borders forces every component to re-add what it needs, and new team members discover these defaults by surprise. A minimal reset changes only the defaults that are genuinely unhelpful in modern layout, and leaves the rest of the browser's sensible behaviour alone.
The test for each rule is simple: what goes wrong without it? If nobody can answer, the rule does not belong. The rules below each come with their answer.
The complete implementation
/* The reset lives in the first layer, so every later layer beats it. */
@layer reset, tokens, base, components, utilities;
@layer reset {
/* 1. Width means the border box. Without this, adding padding to a
width: 100% element makes it overflow its parent. */
*, *::before, *::after {
box-sizing: border-box;
}
/* 2. Remove default margins. Spacing is then decided by layout
(gap, stacks) rather than by per-element defaults that differ
between h1, p, ul and figure. */
* {
margin: 0;
}
/* 3. Let percentage heights resolve against the viewport-height root,
and give the body a sensible minimum so short pages fill it. */
html { block-size: 100%; }
body { min-block-size: 100%; }
/* 4. Readable defaults for body text; inherited everywhere. */
body {
line-height: 1.5;
-webkit-text-size-adjust: 100%; /* stop mobile Safari inflating text in landscape */
}
/* 5. Media are blocks that never overflow their container. Inline images
otherwise sit on the text baseline and leave a small gap below. */
img, picture, video, canvas, svg {
display: block;
max-inline-size: 100%;
}
/* 6. Form controls use the page font rather than the system UI font,
and inherit colour so dark themes do not need per-control rules. */
input, button, textarea, select {
font: inherit;
color: inherit;
}
/* 7. Long words and URLs wrap instead of forcing horizontal scroll. */
p, h1, h2, h3, h4, h5, h6, li, figcaption {
overflow-wrap: break-word;
}
/* 8. Headings wrap into balanced lines; paragraphs avoid orphans. */
h1, h2, h3, h4, h5, h6 { text-wrap: balance; }
p { text-wrap: pretty; }
/* 9. Smooth scrolling only for users who have not asked for less motion. */
@media (prefers-reduced-motion: no-preference) {
html:focus-within { scroll-behavior: smooth; }
}
/* 10. A dedicated class hides content visually but keeps it available
to assistive technology — the correct alternative to display:none
for labels and skip links. */
.visually-hidden:not(:focus):not(:active) {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}
Rule 1 is the one every reset agrees on, and the demo shows why. Both boxes are declared width: 100% with the same padding; under the default content-box model the padding is added outside the declared width, so the box is wider than its container at every size.
A few other rules are worth expanding. Rule 3 uses min-block-size rather than block-size on body, so long pages grow normally; the pattern is taken further in Sticky Footer With Flexbox or Grid. Rule 5 makes SVG display: block, which is right for illustrations but not for inline icons in text; icon components should set display: inline-block themselves, and that is fine because components sit in a later layer. Rule 9 applies smooth scrolling through :focus-within on html, which keeps it for in-page navigation while avoiding the odd effect of smooth scrolling on find-in-page.
The key technique: the reset loses every fight
The reason to wrap the reset in @layer reset is precedence. Reset selectors are broad — *, img, input — and some of them, such as .visually-hidden:not(:focus):not(:active), have more specificity than a component author would expect. Outside layers, a component rule like .btn { margin-block-end: 1rem } would still win against * { margin: 0 }, but a component rule written with an element selector might not beat a more specific reset rule. Inside the first layer, every reset rule loses to every rule in every later layer, regardless of specificity.
Declaring the full layer order in one statement at the top of the stylesheet — before any layer has content — fixes the order permanently, even if files are later loaded in a different sequence. The mechanics of layer precedence, and why it removes the need for specificity hacks, are covered in Cascade Layers vs Specificity Hacks.
What the reset deliberately leaves alone
A minimal reset is defined as much by what it omits:
- List styles. Lists keep their bullets and numbers. Navigation lists that should not show bullets set
list-style: nonein their component — and addrole="list"if the list semantics must survive in Safari, which drops list semantics from unstyled lists. - Focus outlines. The browser's focus ring stays until a component replaces it with a designed
:focus-visiblestyle. Removing outlines in a reset is the single most harmful line found in old resets. - Button appearance. Buttons keep their native look; design-system buttons restyle themselves. A reset that strips all button styling makes every unstyled button on the page invisible as a button.
- Heading sizes. Browser defaults for heading sizes are reasonable and are overridden by the design system anyway.
font-sizeonhtml. Settinghtml { font-size: 62.5% }to make rem arithmetic easier overrides users' font-size preferences in a way that is easy to get wrong; leave the root at the user's default and design in rem.
Replacing an old reset in an existing project
Swapping a large legacy reset for a minimal one changes defaults under every component at once, so do it in a way that makes the differences visible.
First, diff the effective defaults. For a representative set of elements — headings, paragraphs, lists, buttons, inputs, images, tables — record the computed styles under the old reset and the new one. Most differences are harmless, but a few will matter: lists regaining bullets, buttons regaining borders, tables regaining default spacing. Each of those tells you a component that was silently relying on the old reset, and each should get the style it needs in its own rule.
Second, move the old reset into a layer before replacing it. Wrapping the legacy file in @layer reset without changing a single rule is a safe first step; if anything changes visually, some component depended on the reset winning against it, which is worth knowing. Then replace the layer's contents with the minimal version.
Finally, run visual regression tests across the key templates and review the differences. Expect a batch of small spacing changes where * { margin: 0 } differs from the old approach, and settle them by giving content areas an explicit flow rule, such as a stack with gap, rather than by adding margins back element by element.
Browser support
Everything in this reset is supported in current Chrome, Edge, Firefox and Safari. Cascade layers are supported in Chrome and Edge 99+, Firefox 97+ and Safari 15.4+; in older engines, @layer blocks are ignored entirely, so wrap the reset in a layer only if your support policy starts at or above those versions, or keep an unlayered copy for older targets. text-wrap: balance is supported in Chrome and Edge 114+, Firefox 121+ and Safari 17.5+, and text-wrap: pretty in current Chromium and Safari; unsupported values are ignored harmlessly. prefers-reduced-motion is supported in Chrome 74+, Edge 79+, Firefox 63+ and Safari 10.1+.
FAQ
Do modern browsers still need a CSS reset? They need much less than they used to. Browser defaults are now consistent across engines, so old normalisation rules are mostly redundant. What remains useful is a handful of opinionated defaults that make layout predictable, such as border-box sizing, zeroed margins and block-level media.
Should a reset remove focus outlines?
No. Removing outlines globally makes keyboard navigation invisible and fails WCAG 2.4.7 Focus Visible. If the default ring clashes with the design, replace it with a custom :focus-visible style rather than removing it.
Why put the reset in a cascade layer?
A reset should lose to every other style. Placing it in the first cascade layer guarantees that any component or utility rule overrides it regardless of specificity, so reset selectors such as *, ::before and ::after never fight component styles.
Is * { margin: 0 } too aggressive?
It removes margins from every element, including paragraphs and headings, which then need spacing from a layout rule such as a stack with gap or a flow utility. Many teams prefer that explicitness; others reset only the elements whose default margins cause trouble. Both are defensible if applied consistently.
Related
- Modern CSS Reset Strategies — the parent guide.
- Cascade Layers for Reset and Tokens — the layer structure this reset sits in.
- Creating Accessible Focus Indicators — what to do instead of removing outlines.
- Flexbox gap and Spacing — spacing content once margins are reset.
Related articles
More pages in the same section.