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.

What each part of the reset prevents Box model rules prevent padding from enlarging widths. Spacing rules prevent unexpected margins. Media rules prevent images overflowing and inline gaps. Form rules prevent inputs using a different font. Typography and motion rules prevent overflowing words and unrequested smooth scrolling. Five groups, five problems Box model box-sizing padding widens boxes Spacing margin: 0 surprise margins Media display: block overflow, baseline gap Forms font: inherit system font in inputs Text and motion overflow-wrap long words, motion

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.

Live demoWhy the reset starts with box-sizing
Both boxes are width: 100% with 1.5rem of padding. The content-box one overflows its dashed container; the border-box one fits at every width. Drag the bottom-right corner to resize the frame in either direction.

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.

Layer order decides before specificity does Five stacked layers from reset at the bottom to utilities at the top. An arrow shows increasing precedence upward. A note explains that a low-specificity rule in the components layer beats a high-specificity rule in the reset layer. @layer reset, tokens, base, components, utilities reset tokens base components utilities wins specificity only matters inside one layer

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: none in their component — and add role="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-visible style. 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-size on html. Setting html { 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 articles

More pages in the same section.