Type something to search...
Writing a Modern CSS Reset in 2026

Writing a Modern CSS Reset in 2026

A lot of the CSS resets still copied into projects today were written for a different web. They patched Internet Explorer quirks, fixed HTML5 elements that old browsers didn't recognize, and zeroed out every margin on a hundred selectors. Browsers have changed enormously since then, and so has CSS itself. We now have cascade layers, logical properties, text-wrap: balance, dynamic viewport units, and a much more consistent baseline across engines.

That means a modern reset can be short. It doesn't need to fight the browser; it just needs to fix the handful of defaults that still get in the way, and set a few sensible starting points. In this guide, we'll build one from scratch, rule by rule, explaining why each line is there. By the end, you'll have a reset you understand well enough to change, which is more valuable than any reset you copy without reading.

Principles for a Modern Reset

Before writing any CSS, it helps to agree on what the reset is for.

  1. Fix real problems, not historical ones. Every rule should address something that still affects current browsers or nearly every project.
  2. Keep it low priority. A reset should never make it harder to style something later. Cascade layers make that easy.
  3. Don't break accessibility. No removing focus outlines, no stripping list semantics, no disabling zoom.
  4. Stay small. If you can't explain a rule, leave it out.

Step 1: Put It in a Layer

Start by declaring your layer order and placing the reset in the lowest layer:

@layer reset, base, components, utilities;

@layer reset {
  /* rules go here */
}

Styles in earlier layers always lose to styles in later layers, regardless of specificity. That means even a reset selector like *:where(:not(svg)) can't accidentally beat a simple .button class in your components layer. It also means unlayered styles, including third-party CSS you haven't put in a layer, beat the reset automatically.

Cascade layers are supported in all current major browsers. Every rule in the rest of this article goes inside @layer reset.

Step 2: Box Sizing

*,
*::before,
*::after {
  box-sizing: border-box;
}

By default, width and height set the size of the content box, and padding and borders are added on top. With border-box, the size you set includes padding and border, which is how most people expect sizing to work. A 300px-wide card with 20px padding stays 300px wide.

This is still the most universally useful rule in any reset.

Step 3: Remove Default Margins

* {
  margin: 0;
}

Browser default margins on headings, paragraphs, lists, figures, and body rarely match a design system, and they're a common source of margin collapsing surprises. Removing them means all spacing is intentional. You'll add spacing back through layout (gap) or a flow utility later.

Notice we're resetting margin only, not padding. Padding defaults matter for things like form controls and lists, and they're easier to live with.

Step 4: Set Up the Root

html {
  -webkit-text-size-adjust: none;
  text-size-adjust: none;
  color-scheme: light dark;
}

@media (prefers-reduced-motion: no-preference) {
  html {
    interpolate-size: allow-keywords;
  }
}

Text size adjustment: mobile Safari can automatically inflate font sizes when a device rotates to landscape. Disabling that inflation keeps your typography consistent. This doesn't prevent users from zooming; it only turns off the automatic adjustment. Some browsers support the unprefixed property and some only the prefixed one, so include both.

Color scheme: color-scheme: light dark tells the browser your page supports both, so built-in UI like scrollbars and form controls follow the user's system preference. If your site only has a light theme, set it to light instead, or leave it out.

interpolate-size: this lets you animate to and from keyword sizes like height: auto, which was previously impossible in pure CSS. It's currently supported in Chromium-based browsers only. In other browsers the declaration is simply ignored, and transitions to auto jump instantly, as they always have. Wrapping it in prefers-reduced-motion: no-preference means users who prefer less motion don't get the new animations.

Step 5: Body Defaults

body {
  min-height: 100dvh;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

min-height: a body at least as tall as the viewport makes sticky footers and full-height backgrounds easier. The dvh unit is the dynamic viewport height, which accounts for mobile browser toolbars appearing and disappearing, unlike the old 100vh, which is often taller than the visible area on phones. Dynamic viewport units are supported in all current major browsers. If you prefer a value that doesn't change as the toolbar moves, 100svh (small viewport height) is the stable alternative.

line-height: the browser default is normal, which is usually around 1.2 and varies by font. 1.5 is more readable for body text and is a WCAG recommendation for paragraphs. Headings can tighten it later.

font smoothing: this only affects macOS, where it switches text rendering from subpixel to grayscale antialiasing. On modern high-density screens the difference is subtle, and many resets now omit it. It's a matter of taste; delete it if you prefer the default.

Step 6: Media Elements

img,
picture,
video,
canvas,
svg {
  display: block;
  max-width: 100%;
}

img,
video {
  height: auto;
}

display: block: images are inline by default, which means they sit on the text baseline and leave a few pixels of space below them for descenders. Making them block removes that gap, which is almost always what you want for layout images.

max-width: 100%: this stops images from overflowing their containers, which is the basis of responsive images.

height: auto: when you include width and height attributes on an image (which you should, to prevent layout shift), the browser uses them to reserve space with the correct aspect ratio. Without height: auto, max-width could shrink the width while the height attribute keeps its original value, distorting the image.

One consequence to know about: inline SVG icons inside text, like an icon inside a button, now become block elements. Give those a class and set display: inline-block or use a flex container on the parent.

Step 7: Form Controls

input,
button,
textarea,
select {
  font: inherit;
  letter-spacing: inherit;
}

button,
select {
  text-transform: none;
}

textarea:not([rows]) {
  min-height: 10em;
}

font: inherit: form controls don't inherit the page font by default. They use a system font at a smaller size, which is why an unstyled button looks out of place. Inheriting the font makes them match the surrounding text.

text-transform: some browsers have applied inherited text transforms to buttons and selects inconsistently, so resetting it avoids surprises.

textarea min-height: a textarea without a rows attribute can be quite small by default. A minimum height gives users more room, while still respecting an explicit rows value if you set one.

If you want form fields that grow with their content, Chromium-based browsers support field-sizing: content:

@supports (field-sizing: content) {
  textarea {
    field-sizing: content;
  }
}

Other browsers ignore it and keep the fixed size, so it's a safe progressive enhancement. It's not in the reset itself, since auto-growing fields are a design decision.

Step 8: Typography

h1,
h2,
h3,
h4,
h5,
h6 {
  text-wrap: balance;
}

p,
li,
figcaption {
  text-wrap: pretty;
}

p,
h1,
h2,
h3,
h4,
h5,
h6 {
  overflow-wrap: break-word;
}

text-wrap: balance evens out line lengths in short, multi-line text, so a heading doesn't end with one lonely word on the last line. It's supported in all current major browsers. Browsers limit it to a small number of lines for performance, which is why it belongs on headings rather than paragraphs.

text-wrap: pretty uses a slower but better line-breaking algorithm to avoid very short last lines in paragraphs. It's supported in Chromium-based browsers and Safari, and ignored elsewhere, where text wraps normally. Either way, it's harmless.

overflow-wrap: break-word lets long, unbreakable strings like URLs wrap instead of overflowing their container. It only kicks in when a word wouldn't otherwise fit, so normal text is unaffected.

Step 9: Lists with Roles

ul[role="list"],
ol[role="list"] {
  list-style: none;
  padding: 0;
}

Many lists in a UI, like navigation menus and card grids, don't need bullets. But removing list-style can cause Safari to stop announcing them as lists to VoiceOver users. Tying the removal to role="list" means you explicitly opt in to both the semantics and the unstyled look:

<ul role="list" class="nav">
  <li><a href="/">Home</a></li>
  <li><a href="/blog">Blog</a></li>
</ul>

Lists in regular content, like a blog post, keep their bullets and padding.

Step 10: Anchor Targets and Motion

:target {
  scroll-margin-block: 5ex;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

scroll-margin: when a user follows a link to #section-id, the browser scrolls the target to the top edge of the viewport. A little margin keeps it from sitting flush against the edge or hiding under a sticky header.

reduced motion: this is a widely used safety net for users who've asked their operating system to reduce motion. It shortens animations and transitions to almost nothing instead of removing them, so any JavaScript waiting on animationend or transitionend still fires.

A note on !important inside a layer: important declarations reverse layer order, so an !important rule in the lowest layer actually beats !important rules in later layers. That's exactly the behavior you want for a user-protecting rule like this one, but it's worth knowing so it doesn't surprise you.

Step 11: An App Root

#root,
#__next {
  isolation: isolate;
}

If you're building a React or Next.js app, isolation: isolate on the root element creates a new stacking context. That keeps z-index values inside your app from competing with portals, modals, and toasts rendered outside it. If your site doesn't use a root element like this, leave this rule out.

The Complete Reset

Here's everything together:

@layer reset, base, components, utilities;

@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  * {
    margin: 0;
  }

  html {
    -webkit-text-size-adjust: none;
    text-size-adjust: none;
    color-scheme: light dark;
  }

  @media (prefers-reduced-motion: no-preference) {
    html {
      interpolate-size: allow-keywords;
    }
  }

  body {
    min-height: 100dvh;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
  }

  img,
  picture,
  video,
  canvas,
  svg {
    display: block;
    max-width: 100%;
  }

  img,
  video {
    height: auto;
  }

  input,
  button,
  textarea,
  select {
    font: inherit;
    letter-spacing: inherit;
  }

  button,
  select {
    text-transform: none;
  }

  textarea:not([rows]) {
    min-height: 10em;
  }

  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    text-wrap: balance;
  }

  p,
  li,
  figcaption {
    text-wrap: pretty;
  }

  p,
  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    overflow-wrap: break-word;
  }

  ul[role="list"],
  ol[role="list"] {
    list-style: none;
    padding: 0;
  }

  :target {
    scroll-margin-block: 5ex;
  }

  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }
}

It's around 90 lines with generous formatting, and every rule has a clear purpose.

What We Deliberately Left Out

A modern reset is defined as much by what it omits:

  • No outline: none. Focus indicators are essential for keyboard users. If you want custom focus styles, add them with :focus-visible in your base layer.
  • No font-size: 100% on everything. Browsers already agree on default font sizes, and setting a root size in pixels would override users' font size preferences.
  • No zeroing of padding everywhere. Only the specific lists that need it get it.
  • No all: unset on buttons. It's tempting, but it removes focus styles and native behaviors along with the visuals. Style buttons explicitly instead.
  • No legacy fixes. Nothing for Internet Explorer, and nothing to make main or article display as blocks. Every current browser already does.

Building on Top of the Reset

With the reset in place, your base layer can add back the spacing and typography your design needs:

@layer base {
  body {
    font-family: system-ui, sans-serif;
    color: #0f172a;
    background: #ffffff;
  }

  h1,
  h2,
  h3 {
    line-height: 1.15;
  }

  .flow > * + * {
    margin-block-start: var(--flow-space, 1em);
  }

  :focus-visible {
    outline: 2px solid #2563eb;
    outline-offset: 2px;
  }
}

Because the reset removed margins, the .flow utility gives long-form content consistent vertical rhythm, and because each concern lives in its own layer, it's always clear where a style comes from.

Conclusion

A reset in 2026 doesn't need to fight browsers the way older resets did. The remaining problems are few: box sizing, default margins, media elements, form control fonts, and some typographic polish. Newer features like text-wrap, dynamic viewport units, and interpolate-size let a reset do a little more than it used to, as long as you treat the less-supported ones as progressive enhancements.

Put it in the lowest cascade layer, keep accessibility intact, and make sure you understand every line. Then it's not a reset you copied, it's the foundation of your own stylesheet. If you're still deciding between writing your own reset and using a library, see CSS Resets vs. Normalize.css: Which Should You Use?

Tags :
Share :

Related Posts

A Complete Guide to CSS Container Queries

A Complete Guide to CSS Container Queries

For more than a decade, responsive design meant one thing: media queries. You asked the browser how wide the viewport was and adjusted your layout ac

Continue Reading
A Comprehensive Guide to Installing Next.js

A Comprehensive Guide to Installing Next.js

Next.js has emerged as a powerful framework for building React applications, offering features like server-side rendering, static site generation, an

Continue Reading
Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

CSS has evolved significantly, and modern tools like clamp(), min(), and max() are powerful game-changers in dynamic styling. If you’ve struggl

Continue Reading