An Interview With Elad Shechter on “The New CSS Reset”

Avatar of Elad Shechter
Elad Shechter on (Updated on )

Hey folks! Elad reached out to me to show me his new CSS reset project called the-new-css-reset. It’s quite interesting! I thought a neat way to share it with you is not only to point you toward it, but to ask Elad some questions about it for your reading pleasure.

Here’s the entire code for the reset up front:

/*** The new CSS Reset - version 1.2.0 (last updated 23.7.2021) ***/

/* Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property */
*:where(:not(iframe, canvas, img, svg, video):not(svg *)) {
  all: unset;
  display: revert;
}

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

/*
  Remove list styles (bullets/numbers)
  in case you use it with normalize.css
*/
ol, ul {
  list-style: none;
}

/* For images to not be able to exceed their container */
img {
  max-width: 100%;
}

/* Removes spacing between cells in tables */
table {
  border-collapse: collapse;
}

/* Revert the 'white-space' property for textarea elements on Safari */
textarea {
  white-space: revert;
}

First, when talking about “CSS resets” we have two approaches:

  • Nicolas Gallagher’s Normalize.css is the gentle approach. Normalize.css is fixing differences between the implementation in different browsers.
  • Eric Meyer’s CSS Reset is the hard approach, saying that in most cases we don’t want basic styles from the browsers, like the font-size value we get from elements like <h1> through <h6>, or the default styles for the <ul> and <ol> list elements. For example, we use the list only for the semantic meaning, and because it helps in other ways for accessibility and SEO.

I love Normalize.css. I think it’s a must-have in any project, even if you prefer the CSS Reset idea.

And why is Normalize.css so important? Normalize.css touches shadow DOM elements that the CSS Reset doesn’t. When looking at Normalize.css, you will find special pseudo-classes like ::-moz-focus-inner, ::-webkit-file-upload-button, and more. It covers so many bases and that’s why I believe Normalize.css is a must-have in any project.

I love the hard CSS Reset as well. I think in most cases we don’t want the basic styles of the browser, and if we need it in a specific place, we will define it according to our need. This brings me to the point that I’m using both Normalize.css and CSS Reset combined. So, Normalize.css is first to load, followed by the hard CSS Reset.

So, why we need a new CSS reset? The CSS resets we have are built on old CSS features. But in the last several years, we’ve gotten new features built specifically for resetting things in CSS, and this got me thinking that now we can create a much more valid CSS reset using these new cutting-edge CSS features.

It seems to me the juiciest bit here is that very first ruleset. Let’s start with that first CSS property and value: all: unset;. That’s what is doing the heavy lifting in this CSS reset yes? How does that work?

all is the most exceptional CSS property because it allows us to reset all the properties that exist in the CSS all at once.

The property accepts several keywords. The two basics are initial and inherit; there are two smarter ones, which are unset and revert. To understand what all: unset does, we need to jump to the fundamental behavior of our CSS properties.

In CSS, we have two groups of properties:

  • Inherited properties group: These are properties that have inheritance by default — mainly typography properties.
  • Non-inherited properties group: These are all other properties that don’t inherit by default, for example, the Box Model properties that include padding, border, and margin.

Like typography properties, we want to keep the inherit behavior when we try to reset them. So, that’s where we’re able to use the inherit keyword value.

/* Will get values from the parent element value */
font-size: inherit;  
line-height: inherit;
color: inherit;

For the other properties in the non-inherited properties group, we want to get their initial value in most cases. It is worth mentioning that the initial keyword computes differently for different properties.

max-width: initial; /* = none */ 
width: initial; /* auto */
position: initial; /* = static */

After we understand the fundamentals as well as the inherit and initial keyword values, we understand that if we want to reset all of properties together, we can’t use them directly with the all property. That’s because, if we reset all of the properties to the initial value, i.e. all: initial, we lose the inherent behavior on the inherited properties group. And suppose we reset all properties with the inherit value. In that case, all the properties get an inheritance — even Box Model properties, which we want to avoid.

That’s why we have the unset value. unset resets the property according to its type. If we use it on an inherited property, it’s equal to inherit; if we use it on a natural non-inherited, it equals initial.

max-width: unset; /* = initial = none */
font-size: unset; /* = inherit = get parent element value */ 

This brings us back to the main feature of my CSS reset. What all: unset does is reset all the inherited properties to the inherit value, and all the other properties in the non-inherited properties group to their initial value.

This operation removes all the default user-agent-stylesheet styles that the browser is adding. To understand these substantial new CSS powers, all of this happened while I was doing only one operation to all HTML elements.

/* 
  Reset all: 
  - Inherited properties to inherit value
  - Non-inherited properties to initial value
*/
* {
  all: unset;
}

And then you follow it up with display: revert; — does all: unset; do things to the display property that would be undesirable?

Short answer: yes. The display property represents the basic structure which we do want to get from our user-agent stylesheet. As we saw in most of our properties, the unset value is doing an excellent job for us, and we reset all properties in one operation.

Now, to understand what the unique revert keyword value is doing for the display property, let’s talk about the two types of styles that we are getting from our browsers. The styles we are getting from our browsers are built from two layers:

  • Layer 1, the CSS initial value: As we already saw, the first layer is the initial values of all our properties in CSS, including the inherit behavior on some of the properties.
  • Layer 2, the user-agent stylesheet: These are the styles that the browser defines for specific HTML elements.

In most cases, when we want to reset things, we want to remove the basics styles of Layer 2. And when we do reset with all: unset, we remove all the styles of the user-agent stylesheet.

But the display property is exceptional. As we already saw, every property in CSS has only one initial value. This means that if we reset the display property to its initial, like on a <div> element or any other HTML element, it always returns the inline value.

Continuing with this logic, we connect the <div> element to the default display: block declaration, which we get from browsers. But we only get this behavior because of Layer 2, the user-agent stylesheet, which defines them. It’s built on the same idea that the font-size is bigger on heading elements, <h1> to <h6>, than any other HTML elements.

div { 
  display: unset; /* = inline */ 
}
span { 
  display: unset; /* = inline */ 
}
table { 
  display: unset; /* = inline */ 
}
/* or any other HTML element will get inline value */

This is, of course, unwanted behavior. The display property is the only exception we want to get from our browser. Because of that, I’m using the unique keyword value revert to bring back the default display value from the user-agent stylesheet..

The revert value is unique. First, it checks if there is a default style for the specific property in the user-agent stylesheet for the specific HTML element it is sitting on, and if it finds it, it takes it. If it doesn’t find it, revert works like the unset value, which means that if the property is an inherited property by default, it uses the inherit value; if not, it uses the initial value.

A diagram of all the CSS reset keywords

Then those two rules are within a ruleset with a selector where you select almost everything. It looks like you’re selecting everything on the page via the universal tag selector (*), but then removing a handful of things. Is that right? Why target those specific things?

When I started to imagine “The New CSS Reset” I didn’t think I would need exceptions. It was a lot more straightforward in my imagination.

But when I started to create experiences, I was replacing my old CSS reset with my new CSS reset (without all the exceptions), and I saw some things that broke my old projects, which I tested.

The main things that broke were elements that can get sizes via width and height attributes — elements like <iframe>, <canvas>, <img>, <svg>, and <video>. Unfortunately, when I reset everything, the width and height of those elements are defined by the auto value, which is stronger and removes the effect of the elements’ width and the height attributes.

This can be problematic because we want the exact size to come from the HTML element in cases where we add the dimensions via the HTML width and height attributes. We prefer to get it from the HTML, not from the CSS, because when it comes from the CSS, it can cause glitches when the page is loading.

The only way I found to remove the reset effect for all those particular elements is to put them under the :not() selector. In this case, my new CSS reset is harmful and not helpful, and because of that, I removed the effect for these specific elements.

Keeping specificity at a minimum seems important in a reset, so you don’t find yourself fighting the reset itself. Is that the idea behind :where()?

Yes, the idea of the :where() is to remove the specificity. We don’t need to describe more significant specificity in our CSS only to override the CSS reset.

In general, I think we will soon see a lot more cases of :where() wrapping things to remove their specificity, and not only to replace multiple selectors.

It looks like some extra special care for children of <svg> is in there. What is that about?

The second case, :not(svg *) is done with a separate :not() only because it is for a different issue. Touching the inner elements of an SVG can break the visual image, and this is one of those things that there isn’t any reasonable cause to interrupt the browser.

Let the image be an image. I say.

After the big resetting part, it goes into some bits that are more opinionated. For example, there are no browser disagreements about the initial value of box-sizing, but you’re changing it anyway. I’m a fan of that one myself, but I’m curious about the philosophy of what goes into a reset and what doesn’t.

In general, when it comes to a CSS reset, I think it is an opinion thing. For example, Eric Meyer’s CSS Reset chooses to remove the styles of specific things, and other things like the display property, are uninterrupted, which as you already saw, I totally agree with.

About box-sizing, yes, that is opinionated. I have been a web developer for 15 years. In that time, I’ve seen many web developers struggling to understand the default behavior of box-sizing, which I got so used to in the past. When there were talking about adding it to the CSS Reset many years ago, web developers, many of whom had been in the industry for a long time, were afraid of this change because, in general, people are scared of change.

But these days, I almost do not see any project that isn’t resetting all elements to box-sizing: border-box. A browser’s engines can’t fix the default awkward behavior of the default box-sizing: content-box, because if they do so, they will break support for older websites. But for newer projects, including this piece is a must since we’re left to solve it on our own.

And again, this is totally opinionated.

Two other rulesets, the removing of list styles and collapsing borders, are also in the Eric Meyer’s reset, so they have been around a long time! Starting with the list styles, I can see wanting to wipe those out as lists are often used for things that don’t need a marker, like navigation. But it feels a bit contentious these days, as list-style: none; wipes out the semantics of a list, as well on iOS. Any concerns there?

The short answer: no. No concerns on my end. Here’ why.

If we choose not to reset list-style, it means we can’t use list elements for navigation. This also means that we won’t get any semantics for any other browsers.

And now, if I need to choose between most browsers gaining these semantics, and no browsers gaining these semantics, I’m choosing the former, as more browsers gain from it than they lose.

Can you see yourself adding to this over time? Like if you find yourself doing the same things on projects over and over? Setting the max-width on images feels like that to me. Again, it’s not something browsers disagree on now, but also something that pretty much every project does.

Of course. If this reset is missing something that I didn’t consider, I will add it and release a new version. But it needs to be like your example of max-width where there is no good case where we want an image to overflow its container.

Have you seen this new Cascade Layers stuff? Any thoughts on how that might factor in to CSS resets down the road?

I didn’t think about it until you asked me. The Cascade Layers module is an exciting feature. It still doesn’t have any support, but most browser engines have already put this feature under a flag, and this means that there is a good chance that we will see this feature one year from now supported in all evergreen browsers.

For those who haven’t heard about Cascade Layers yet, the idea is that @layer can override styles without creating stronger specificity because every layer that loads after it is automatically stronger than the previous layers.

When this feature arrives, we will load the “reset” layers first. For example: first, Normalize.css, then the-new-css-reset, and then the @layer styles of the project.

@layer normalize; /* Create 1st layer named “normalize” */
@layer the-new-css-reset; /* Create 2nd layer named “the-new-css-reset” */
@layer project-styles; /* Create 3rd layer named “project-styles” */

This should make sure that the bottom layer always beats the top. This also means that removing specificity with :where(), like I did, will no longer be necessary.

@layer is one of the most exciting future features coming to CSS, thanks to Miriam Suzanne, who is doing, as always, a fantastic job.

Thanks for taking the time Elad!