The Coyier CSS Starter

Chris Coyier Chris Coyier on

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

  1. 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.
  2. 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.
  3. Use only logical properties. It’s a net gain.
  4. Don’t use --custom-properties. Setting those up is a step too far for this starter. Use Open Props for that sort of thing.
  5. Use @layer because “you’ll need to do it anyway if you ever want to use layers anywhere else.”
  6. Do accessibility things that are easy to forget about, but nothing so niche it doesn’t come up for me often/ever.
  7. 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.
  8. 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.
  9. 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)

You know I had to.

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.

Wanna learn CSS from a course?

Frontend Masters logo

FYI, we have a full CSS learning path with multiple courses depending on how you want to approach it.

7-Day Free Trial

20 responses to “The Coyier CSS Starter”

  1. 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

    • Mike Mai says:

      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.

  2. Might wanna update your [hidden] selector to allow for hidden=until-found which uses content-visibility: hidden instead of display: 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.

    • Chris Coyier says:

      Would you do like….?

      [hidden]:not([hidden="until-found]) {
        display: none !important;
      }
      
      • 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.

        /* we’re assuming this is the first layer */
        @layer reset {
          [hidden] {
            /* future-proof hidden reset */
            display: revert-layer !important;
          }
        }
        
  3. 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*

    • Chris Coyier says:

      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

      • Mike Mai says:

        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.

  4. Adam Shand says:

    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;

  5. Great css starter!

    But shouldn’t the link color use light-dark() so that it remains accessible in both color-scheme modes ?

  6. Mike Mai says:

    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.

  7. Nicklas says:

    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.

  8. 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:

    [role="region"][aria-labelledby][tabindex] {
      overflow: auto;
    }
    

    Solution, this a useful classless solution for a overflowing tables, something I should add to the Fylgja Base.
    So thanks for sharing 😊

    • Chris Coyier says:

      not sure if I would put the default view-transition in a prefers-reduced-motion, since this not considered a harmful animation.

      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.

  9. Matt says:
      p {
        max-inline-size: 88ch;
      }
    

    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)?

    • Chris Coyier says:

      Makes me think about how it would be interesting if style-queries worked for more than custom properties, so like

      p {
        @container not style(text-align: center) {
          max-inline-size: 88ch;
        }
      }
      

      (That’s not actually a real thing, just would be neat.)

Leave a Reply to Antoine Villepreux Cancel reply

Your email address will not be published. Required fields are marked *

$839,000

Frontend Masters donates to open source projects through thanks.dev and Open Collective, as well as donates to non-profits like The Last Mile, Annie Canons, and Vets Who Code.