I felt called out by Mike Mai’s You are not a CSS dev if you have not made a CSS reset.
I hadn’t! I mean, I’ve used * { box-sizing: border-box; }
a ton and, way back, I used to use that same universal selector to wipe away margins and padding, which was the inspiration behind the CSS-Tricks logo, believe it or not.
But I never made or published anything and planted a stake in the ground and said “This one. This one is mine.”
So I’m going to do that.
First, I think it’s important to lay some ground rules. Some principles, really. There are a lot of directions a thing like this can go, and without what it’s for (and not for) it’s fairly useless.
Principles
- This is for me. The styles in here are useful to me. They are things I find myself doing very often (or forgetting to do.) I’d like to be using this in most demos I make and dipping into it for any future project. I do hope y’all will find some value in it too of course, hence blogging about it, but as a guiding principal it’s for me.
- This is not a reset, it’s an opinionated starter. I’m not trying to wipe out all existing styles from user-agent stylesheets. I wipe out the ones that are useful for me to remove. It’s more about adding useful styles, improving UX broadly, and fixing common issues.
- Use only logical properties. It’s a net gain.
- Don’t use
--custom-properties
. Setting those up is a step too far for this starter. Use Open Props for that sort of thing. - Use
@layer
because “you’ll need to do it anyway if you ever want to use layers anywhere else.” - Do accessibility things that are easy to forget about, but nothing so niche it doesn’t come up for me often/ever.
- Don’t do too much. None of this is strictly necessary so if it feels too weighty I’d probably find myself not using it. This whole thing is pointless if I don’t use it. Doing nothing is totally an option, so if this doesn’t feel more useful than just defining styles as you go, it’s a fail.
- This isn’t a thing anybody is going to
npm install
. It’s not versioned nor meant to be always used wholesale. Copying out useful bits is fine usage. - This is a list of things I almost wish were the browser defaults (i.e. in the user agent stylesheet) if I ruled the world and didn’t have to care about backwards compatibility.
The Whole Thing
You can see it on this Pen, or directly as a file here.
@layer reset {
html {
color-scheme: light dark;
font:
clamp(1rem, 1rem + 0.5vw, 2rem) / 1.4 system-ui,
sans-serif;
tab-size: 2;
hanging-punctuation: first allow-end last;
word-break: break-word;
}
body {
margin: 0;
padding: 2rem;
@media (width < 500px) {
padding: 1rem;
}
}
*,
*::before,
*::after {
box-sizing: border-box;
}
h1,
h2 {
font-weight: 900;
letter-spacing: -0.02rem;
}
h1,
h2,
h3 {
line-height: 1.1;
}
h1,
h2,
h3,
h4,
h5,
h6 /* FUTURE :heading */ {
text-wrap: balance;
margin-block-start: 0;
}
p,
li,
dd {
text-wrap: pretty;
max-inline-size: 88ch;
}
a {
color: oklch(0.68 0.17 228);
text-underline-offset: 2px;
&:not(:is(:hover, :focus)) {
text-decoration-color: color-mix(in srgb, currentColor, transparent 50%);
}
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
inset-block-end: -0.25em;
}
sup {
inset-block-start: -0.5em;
}
ul,
ol,
dl {
margin: 0;
padding: 0;
list-style: inside;
ul,
ol,
dl {
padding-inline-start: 2ch;
}
}
img,
video,
iframe {
display: block;
max-inline-size: 100%;
block-size: auto;
border-style: none;
}
figure {
inline-size: fit-content;
margin-inline: auto;
}
figcaption {
contain: inline-size;
font-size: 90%;
}
input,
select,
textarea,
button {
font: inherit;
/* FUTURE: appearance: base; */
}
label {
display: block;
}
input:not(
:where(
[type="submit"],
[type="checkbox"],
[type="radio"],
[type="button"],
[type="reset"]
)
) {
inline-size: 100%;
}
button,
input:where(
[type="submit"],
[type="reset"],
[type="button"]
) {
background: CanvasText;
color: Canvas;
border: 1px solid transparent;
}
textarea {
field-sizing: content;
min-block-size: 5lh;
inline-size: 100%;
max-inline-size: 100%;
}
pre,
code,
kbd,
samp {
font-family: ui-monospace, SFMono-Regular, monospace;
}
svg {
fill: currentColor;
}
[aria-disabled="true" i],
[disabled] {
cursor: not-allowed;
}
[hidden] {
display: none !important;
}
[disabled],
label:has(input[disabled]) {
opacity: 0.5;
[disabled] {
opacity: 1;
}
}
pre {
white-space: pre-wrap;
background: CanvasText;
color: Canvas;
padding: 1.5rem;
}
hr {
border-style: solid;
border-width: 1px 0 0;
color: inherit;
height: 0;
overflow: visible;
margin-block: 2.5rem;
}
:target {
scroll-margin: 3rlh;
}
table {
caption-side: bottom;
border-collapse: collapse;
td {
font-size: 90%;
}
td,
th {
word-break: normal;
border: 1px solid gray;
padding: 0.5rem;
}
}
[role="region"][aria-labelledby][tabindex] {
overflow: auto;
}
caption {
font-size: 90%;
}
.screenreader-only:not(:focus):not(:active) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
:focus-visible {
outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
@view-transition {
navigation: auto;
}
html {
interpolate-size: allow-keywords;
&:focus-within {
scroll-behavior: smooth;
}
}
}
}
Code language: CSS (css)
Notes about the Choices
Layering
@layer reset {
}
Code language: CSS (css)
There is no usage of :where()
in here just to lower specificity. The usage of :where()
is only to write selectors that are less repetitive and easier to read. Specificity is already bottomed out due to the @layer
, and people who choose to use this can position the layer where they want to.
I was convinved this was a good idea from Mayank’s Your CSS reset should be layered.
Color Scheme
html {
color-scheme: light dark;
}
Code language: CSS (css)
Putting color-scheme: light dark;
in there is a relatively big choice, because it opts you into needing to deal with both themes for nearly all color/background-color usage. But that’s good. People like it when at least their operating system choice is honored. Just this basic thing alone buys dark-mode scrollbars and checkboxes and inputs and stuff, so it’s worth doing. Now light-dark()
will work so implementing scheme-specific colors isn’t so bad.
Clamped Root Font Size
html {
font: clamp(1rem, 1rem + 0.5dvw, 2rem) / 1.4 system-ui, sans-serif;
}
Code language: CSS (css)
Controversial?! Maybe. But because clamp(1rem, 1rem + 0.5dvw, 2rem)
involves relative units, it should still scale properly with users adjusting their base font size. I feel like setting a fluid font size here (and nowhere else) percolates pretty nicely throughout the document and feels like visually.
That’s a juiced up line-height
and system-ui
as the font which tends to look nice everywhere. Just this alone speeds up demo-making where I just can’t look at the default Times New Roman very long. Nothing against that typeface, it just makes demos look un-cared for.
Tab Size
html {
tab-size: 2;
}
Code language: CSS (css)
The fact that the default tab-size
is 8 is whackadoo and needs to get tamped down. I get that taking advantage of wider screens can feel good but narrow screens are so much more common. Maybe I should use a dvi
unit??
Body Spacing
The body has an 8px
margin which is just awkward. I feel good about a more generous 2rem
padding kicking down to 1rem
at smaller screens. 500px is a heck of a magic number there, but feels generally right to me — it doesn’t have to sync up with other layout changes.
body {
margin: 0;
padding: 2rem;
@media (width < 500px) {
padding: 1rem;
}
}
Code language: CSS (css)
Box Sizing
*,
*::before,
*::after {
box-sizing: border-box;
}
Code language: CSS (css)
The ultimate classic thing for a reset. When I don’t do it up-front, I very regularly find myself needing to circle back and add it. I originally went with the inheritance model, but Miriam convinced me not to, basically by saying it “solves a problem that doesn’t really exist.”
Hanging Punctuation
html {
hanging-punctuation: first allow-end last;
}
Code language: CSS (css)
It’s just a little nicer looking so I’m taking my own advice.
Preventing Breakouts
html {
word-break: break-word;
}
pre {
white-space: pre-wrap;
}
Code language: CSS (css)
These two are protective bits that essentially prevent long text (whether it wouldn’t naturally break or would be otherwise told not to break) from pushing a container wider than its parent would naturally be.
Headers
h1,
h2 {
font-weight: 900;
letter-spacing: -0.02rem;
}
h1,
h2,
h3 {
line-height: 1.1;
}
h1,
h2,
h3,
h4,
h5,
h6 /* FUTURE :heading */ {
text-wrap: balance;
margin-block-start: 0;
}
Code language: CSS (css)
The biggest two headers have a juiced up weight and are tucked in a little bit. The biggest three reduce their line-height
(1.4, like the rest of the document, is too much for large headers). All the headers (which can be selected with :heading
in the future) have wrapped balancing and have the margin above them knocked out. I tried leaving most of the user-agent stylesheet styles for stuff like this alone, but this one always annoys me.
Pretty Type
p,
li,
dd {
text-wrap: pretty;
max-inline-size: 88ch;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
inset-block-end: -0.25em;
}
sup {
inset-block-start: -0.5em;
}
pre,
code,
kbd,
samp {
font-family: ui-monospace, SFMono-Regular, monospace;
}
Code language: CSS (css)
We balanced the headings above, but we can use pretty
on paragraph-y text for better typography. I’m also limiting the line length by limiting size in the direction of the flow. That 88ch
still probably too wide for most text. I just picked the Back to the Future number, but it’s not without merit). I’m not trying to be ultra-opinionated in this case, just protective.
Those sub
and sup
styles date all the way back to the original Normalize. I’m a fan because otherwise those elements can cause an extra-thick line-height in the middle of a paragraph and it’s quite unsightly.
Chilled Out Underlines
a {
color: oklch(0.68 0.17 228);
text-underline-offset: 2px;
&:not(:is(:hover, :focus)) {
text-decoration-color: color-mix(in srgb, currentColor, transparent 50%);
}
}
Code language: CSS (css)
Lists
ul,
ol,
dl {
margin: 0;
padding: 0;
list-style: inside;
ul,
ol,
dl {
padding-inline-start: 2ch;
}
}
Code language: CSS (css)
I like my list bullets inside the container and nested lists indented just a bit, matching the tab-size
.
Media
img,
video,
iframe {
display: block;
max-inline-size: 100%;
block-size: auto;
border-style: none;
}
figure {
inline-size: fit-content;
margin-inline: auto;
}
figcaption {
contain: inline-size;
font-size: 90%;
}
Code language: CSS (css)
I’m being protective here with my media elements, making sure they don’t break out of a container. Then if you are potentially limiting the width (inline-size), you need to let the height be free (block-size) so you don’t get squishing.
The figure/figcaption stuff is from this whole journey and I think it makes sense broadly.
Forms
input,
select,
textarea,
button {
font: inherit;
/* FUTURE: apperance: base; */
}
label {
display: block;
}
input:not(
:where(
[type="submit"],
[type="checkbox"],
[type="radio"],
[type="button"],
[type="reset"]
)
) {
inline-size: 100%;
}
button,
input:where(
[type="submit"],
[type="reset"],
[type="button"]
) {
background: CanvasText;
color: Canvas;
border: 1px solid transparent;
}
textarea {
field-sizing: content;
min-block-size: 5lh;
inline-size: 100%;
max-inline-size: 100%;
}
Code language: CSS (css)
The first bit has form elements use the same basic typography as the rest of the site, which is a big improvement for consistency in a UI if you ask me. This is opinionated toward single-column full-width forms where research convinced me is just better for users. Buttons flop out light/dark colors and have an (invisible) border (which becomes visible in Windows High Contrast mode, so it’s an accessibility thing).
Using field-sizing
on textareas is just correct, but you need a little extra there to make sure it doesn’t collapse.
SVG
svg {
fill: currentColor;
}
Code language: CSS (css)
I’m not doing much for SVG. That could evolve. But for now I do like it when SVG icons color comes along for the ride and dare-I-say most icons use fill
rather than stroke
for their main coloring.
Hidden (is a lie)
[hidden] {
display: none !important;
}
.screenreader-only:not(:focus):not(:active) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
:focus-visible {
outline-offset: 2px;
}
Code language: CSS (css)
Monica Dinculescu classic. I think it’s worth being able to use (and trust) this attribute.
Plus, you gotta have an accessible hiding class. As a little bonus I think it looks a little nicer to push the default outlines away from elements a bit when they are tabbed to (with the keyboard).
Horizontal Rules
hr {
border-style: solid;
border-width: 1px 0 0;
color: inherit;
height: 0;
overflow: visible;
margin-block: 2.5rem;
}
Code language: CSS (css)
I hate the default weird beveled look. I just want a line with lots of space above and below it.
No Headbutting
:target {
scroll-margin: 3rlh;
}
Code language: CSS (css)
I also hate it when you #hash-link to something and it’s smashed to the edge of the browser window, potentially even hurting the context of why you’re linking to it. This is a magic number, but it feels good usually.
Tables
table {
caption-side: bottom;
border-collapse: collapse;
td {
font-size: 90%;
}
td,
th {
word-break: normal;
border: 1px solid gray;
padding: 0.5rem;
}
}
[role="region"][aria-labelledby][tabindex] {
overflow: auto;
}
caption {
font-size: 90%;
}
Code language: CSS (css)
This kicks the <caption>
to the bottom of the table, where you’d normally put a <figcaption>
so it just looks/feels better. Then it smashes the border
of table cells together which just feels correct. Then it sets you up for proper very basic responsive tables. Again, protective stuff.
Motion
@media (prefers-reduced-motion: no-preference) {
@view-transition {
navigation: auto;
}
html {
interpolate-size: allow-keywords;
&:focus-within {
scroll-behavior: smooth;
}
}
}
This opt-in’s to multi-page view transitions which you probably almost always want, but only does it if the user hasn’t said they prefer reduced motion through their operating system. I think that’s highly appropriate here because it can make the whole screen move which is the riskiest of all motion-sickness stuff in my experience.
This also opts-in to animate-to-auto which just rules and allows for smooth-scrolling, but only when the page is focused (so doesn’t do that when you’re using “find” in the browser itself to search the page.
Critique?
What do you like and dislike? What would you do differently? I love scrutinizing other people’s starters/resets, so I’m certainly open to the same on mine.
Thanks for sharing!
Some awesome things in here, some I’ve never seen before.
A few small thoughts – WCAG does have a mention in 1.4.8 about a max line length of 80 characters. I use this (80ch) to avoid the ‘magic number’ feeling on max-inline-size (https://www.w3.org/TR/WCAG22/#visual-presentation)
Also curious – any reason not to include [role=‘heading’] or legend in the h1-6 reset?
As for the :target magic number, I personally would probably try to find a way to have that visual logic follow body/element/page spacing logic, so the distance could be inherited – but that’s just me.
Great article, there’s a lot I’ve missed apparently! Excited to learn
Hey Chris. Long time no see since Pega.
I want to clarify about 1.4.8. I don’t believe the SC actually mandates a character count. It just means that if the user decides to limit it themselves, they should be able to and the content is still accessible.
I’m sure you know this, but just FYI for anyone else who is not familiar with the WCAG.
Might wanna update your
[hidden]
selector to allow forhidden=until-found
which usescontent-visibility: hidden
instead ofdisplay: none
.I have a weak opinion that enforcing this sort of thing might be a misstep. Checking for presentation hints when we write our selectors isn’t a bad idea. Allows for more flexibility later on. But again, weakly held opinion.
Would you do like….?
That’s probably fine, despite not being future proof. It will need to be updated whenever new attribute values are added. It’s not like we frequently get new attributes values though.
In theory, if you could guarantee that your reset was the first layer, then you could make sure of
revert-layer
instead, since this would go back to the presentational hints.revert
is too strong — it scrubs presentational hints.Is there an extraordinarily good reason I’m missing for using dvw in the root font-size rather than just vw? dvw changes size when some parts of the UI are shown/hidden. That’s not something you want your don’t to respond to! It’s meant for things that are actually sized to the viewport and need to both fill and stay fully visible as much as possible. For everything else, use sv*, lv*, or just v*
There really isn’t a good reason. The reason was just “dvh good” for settings heights, so “dvw must be good”, but having some browser chrome adjusting font size does seem kinda bad. I’ll update
i have a good reason for using dynamic unit. browsers toolbar could be positioned top, right, bottom, or left, and it can show or hide. the “dynamic” takes this into consideration.
in my reset, i use
dvi
in the clamp, which is the logical unit, instead of width, it is inline-size. i do this because i am always thinking internationally with different languages and writing directions.As a lover of dark mode, and a hater of being momentarily blinded by the white flash as pages load when I’m working late … I really wish people would use:
color-scheme: dark light;
I didn’t know that worked! Does that like somehow prefer the dark mode and prevent a white flash? That’d be a good blog post.
i do, and i didn’t know that actually works! need to go test it now.
Great css starter!
But shouldn’t the link color use light-dark() so that it remains accessible in both color-scheme modes ?
That’s a good thought, and, maybe? I did intentionally pick a blue color I thought worked pretty decently across both, but some tweaking might be nice. I’d probably do this: https://frontendmasters.com/blog/tweaking-one-set-of-colors-for-light-dark-modes/
Hey Chris. I was just listening to your podcast episode about CSS reset the other day, glad to finally see this released.
I now shall call you a CSS Dev. HAHAHAHA.
Nice!
I been fiddeling with a kind of Codepen starter for a while, trying to balance simplicity & speed vs complexity.
I would probably only change all the media queries to responsive units so they flip ”together” with all the other responsive units.
Looks nice, not sure if I would put the default view-transition in a prefers-reduced-motion, since this not considered a harmful animation.
I like the:
Solution, this a useful classless solution for a overflowing tables, something I should add to the Fylgja Base.
So thanks for sharing 😊
It kinda feels like it could be harmful to me as it affects the entire page (by default). Even thought it’s just a fade, not movement, it feels like it could be a lot for some. This is just a hunch and not backed by anything.
I like setting this too – the one annoyance is that if the text is centralised, you also have to add
margin-inline: auto
so that both paragraph element and the text are centralised (I say “have to” – there’s various alternative solutions).Which gets more complicated if you want text left-aligned and centralised at different breakpoints.
What’s the easiest way to manage it? Can the new
@if
syntax sort this (at least once it has better browser support)?Makes me think about how it would be interesting if style-queries worked for more than custom properties, so like
(That’s not actually a real thing, just would be neat.)