css
Making the case for CSS normalize and reset stylesheets in 2023
If you’re embarking on writing CSS without a framework, adopting a normalize and reset stylesheet should be your first step. Here’s what I recommend.
I often find myself writing CSS without a framework. One thing that always comes up early in a project is whether to use a CSS reset, some form of normalize, both, or nothing at all.
After experimenting with all of these in recent projects, I’ve now settled on my preferred solution: modern-normalize plus a very lightweight set of reset styles. If you’re curious, you can jump straight to the code.
Along the way, I reeducated myself on what reset and normalize stylesheets are, why we need them in the first place, and how modern CSS frameworks are solving the problem. Here is what I learned.
The challenge of default stylesheets
Each browser ships with its own default stylesheet that sets fonts, text sizes, weights, colors, margins, and so on. These default stylesheets are important because they ensure that the web is legible, accessible, and responsive by default, even for HTML pages that have no CSS of their own.
However, these browser stylesheets present a challenge to frontend developers because:
- Each browser has a subtly different and opinionated take on what the defaults should be. In practice, modern browsers are 99% similar, but the 1% differences can lead to frustrating surprises.
- Some browser stylesheets implement weird, undesirable behavior, like
-webkit-text-size-adjust
, or are missing styles for certain elements, like<abbr>
. - Very often developers want to use a certain element, like
<h1>
or<ul>
for its semantic meaning, but don’t want all of the associated default styles. The default margins attached to most elements, for example, almost never match up with what a developer wants. This makes writing CSS sometimes feel like a “whack-a-mole” exercise of repeatedly overriding undesirable defaults. - Finally, there are defaults defined by the CSS specification itself that make the behavior of CSS harder to understand, like
box-sizing: content-box
.
content-box
model produces maddening results when border and padding are involved, as shown in this example from MDN.How to fix it
To fix the problems inherent in browser default stylesheets, frontend developers have traditionally taken three different approaches: normalize the CSS, reset it, or a combination of both.
Normalize
Applying a normalize stylesheet means fixing the inconsistencies between browsers while still broadly retaining their default set of styles. In other words, it embraces the goals of the browser default stylesheet – provide a legible, accessible, and responsive experience out of the box – while making CSS development more predictable across browsers.
Nicolas Gallagher’s normalize.css popularized this approach.
Reset
On the other hand, a reset stylesheet is the nuclear option: it removes margins, paddings, and text styles across the board. HTML elements completely lose their visual differentiation: <p>
no longer looks like a paragraph, <h1>
no longer looks like heading, <ul>
is no longer a bulleted list, and so on. HTML pages styled with a reset stylesheet are no longer very legible or accessible; the onus is now on the developer to implement a design to add those capabilities back.
This approach is pretty severe, but it has its advantages because it removes the “whack-a-mole” problem of CSS completely. It turns the web into a blank canvas, such that the CSS you write is now purely additive, rather than potentially in conflict with something preexisting.
Eric Meyer’s Reset CSS was an early and influential reset.
Hybrid
Fast-forward to 2023, and many frameworks now take a hybrid approach. Tailwind CSS is a good example: it ships with something called Preflight that is a combination of a normalize and reset stylesheet.
The idea is that you want the bug fixes and remediation of browser oddities that are typically part of a normalize stylesheet, plus the zero-margin clean slate that is the job of a reset stylesheet. Together these provide the blank canvas developers want, and without any browser surprises.
Unlike a pure reset, these hybrid-reset stylesheets will often add some opinionated defaults of their own, like setting box-sizing: border-box
or adding img { max-width: 100% }
to make images more responsive.
border-box
, CSS layout becomes much more intuitive.My recommendation
Browsers have improved in recent years and the differences between them are small, but there are still enough surprises that a normalize stylesheet is worthwhile. Following in the footsteps of Tailwind’s Preflight, I recommend using a concise, well-tested library like modern-normalize.
In terms of a reset, most off-the-shelf resets do too much. Figure out what browser defaults cause you the most grief and target just those elements and properties with your own reset.
Here’s my typical reset stylesheet in its entirety:
@import "modern-normalize";
:root {
line-height: 1.5;
}
h1, h2, h3, h4, h5, figure, p, ol, ul {
margin: 0;
}
ol, ul {
list-style: none;
padding-inline: 0;
}
h1, h2, h3, h4, h5 {
font-size: inherit;
font-weight: inherit;
}
img {
display: block;
max-inline-size: 100%;
}
I’ll expand on my reasoning below.
modern-normalize
@import "modern-normalize";
The modern-normalize library does the traditional job of a normalize stylesheet, plus adds some opinionated defaults of its own. It’s available as an npm package, or you can grab the CSS directly.
Here’s what I appreciate about modern-normalize:
- It disables the weird
-webkit-text-size-adjust
behavior on iOS that causes text to zoom when the device is rotated from portrait to landscape. - It sets
system-ui
as the default font (with fallbacks) for a clean, modern look; much nicer than Times New Roman. - It ensures that form inputs and buttons match the document’s font and text size (
font-family: inherit
andfont-size: 100%
). - It removes the default margin on the
<body>
element. - It globally applies
box-sizing: border-box
. Yay!
Otherwise, modern-normalize tracks browser defaults very closely and is relatively un-opinionated, as compared to something like sanitize.css.
Improved line height
The default line-height provided by browsers (and retained by modern-normalize) is 1.15, which is much too small. This value might work for extremely large headlines or narrow UI labels, but not for most use-cases.
As Josh Comeau points out in his CSS reset article, WCAG criteria states that line-height should be at least 1.5. I agree!
:root {
line-height: 1.5;
}
Clearing margins
Paragraphs and headings are some of the most commonly used elements when building web apps. After all, user interfaces have quite a lot of text. In practice, I find that I am always fighting against the default margins of these elements.
h1, h2, h3, h4, h5, figure, p, ol, ul {
margin: 0;
}
Resetting heading typography
Headings define the document outline of a page, and are important for accessibility. The title of the page should be an <h1>
, the next important heading in the hierarchy of the page should be an <h2>
, and so on.
However, when designing UIs and layouts, the most important heading is not always a consistent size from page to page. And depending on the font, bold might not always be the desired weight.
h1, h2, h3, h4, h5 {
font-size: inherit;
font-weight: inherit;
}
Remove default list styling
Lists of items are very common in web apps, but rarely do I want them represented by indented bullets or numbers (unless it is long form-content, like a blog post or knowledge base article; see below).
ol, ul {
list-style: none;
padding-inline: 0;
}
Responsive images
Modern phones have 2x and 3x hi-dpi displays. That means an area of the page measured as 300px
in CSS, for example, will need 600 or 900 device pixels worth of image data to look nice and crisp. In other words, I often find that I need to “squeeze” large images into smaller spaces.
However, the default browser <img>
styling will result in a large image simply overflowing, which is not desirable.
img {
display: block;
max-inline-size: 100%;
}
What if you need to “undo” a reset?
Sometimes clearing a set of styles is the right decision 95% of the time, but there is that 5% use-case where I really wish I had the browser’s default styles. Lists are a good example: most of the time I don’t want to see bullets or indentation. But if I am formatting long-form content, like a terms-and-conditions page, I’ll want to “undo” the reset.
In those cases you can use the CSS revert
keyword.
.article :where(ol, ul) {
all: revert;
}
.article
, <ol>
and <ul>
are displayed using the original browser default styles, with appropriate bullets, numerals, margins, and padding, thanks to revert
.
Conclusion
When choosing your approach to CSS on a new project, consider the following:
- Is a CSS framework like Tailwind a good fit for the project? Frameworks typically bundle their own normalize and/or reset.
- Writing CSS from scratch? Start by adding modern-normalize. This gives you
border-box
and will fix bugs, accessibility issues, and other undesirable behavior that might otherwise go unnoticed during development and end up impacting your users. - If you are going for a browser-default aesthetic, then you might not need a CSS reset. Otherwise seriously consider adding one, like the lightweight version below. It will save you a lot of repetitive styling in the long run. Also, it can be risky to add a CSS reset late in a project, so this is best decided up front.
@import "modern-normalize";
:root {
line-height: 1.5;
}
h1, h2, h3, h4, h5, figure, p, ol, ul {
margin: 0;
}
ol, ul {
list-style: none;
padding-inline: 0;
}
h1, h2, h3, h4, h5 {
font-size: inherit;
font-weight: inherit;
}
img {
display: block;
max-inline-size: 100%;
}