Entry 01

The reset

Start every new project with it. In an existing project add one rule at a time and look at the result, because min-width: 0 and the font rules change how things already render.

Open
Make the frame narrower. The two halves stay equal, the heading stays balanced, and the wide image stays inside.
Code CSS · 40 lines
*,
*::before,
*::after {
  box-sizing: border-box;
  min-width: 0;
}

:root {
  interpolate-size: allow-keywords;
  scrollbar-gutter: stable;
  text-wrap: pretty;
  overflow-wrap: break-word;
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

body {
  margin: 0;
  min-height: 100svh;
}

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

img, svg, video {
  display: block;
  max-width: 100%;
  height: auto;
}

input, textarea, select { font-size: max(16px, 1rem); }

button, a, [role="button"] { touch-action: manipulation; }

button, [role="button"] {
  user-select: none;
  -webkit-user-select: none;
}
Why it works, the rules and the credits 14 reasons · 8 rules · support · credits

Why it works

  1. min-width: 0 lets flex and grid items shrink below their content. Their default is min-width: auto, which is why a long word or a wide child overflows a grid.
  2. interpolate-size: allow-keywords turns on transitions to and from auto for the whole page. Accordion that animates its height depends on it.
  3. text-wrap: pretty tells the browser to spend more effort on line breaks. It avoids a last line that holds one short word, and Safari also evens out the ragged edge. The property inherits, so :root reaches every element.
  4. text-wrap: balance gives headings lines of similar length.
  5. font-synthesis: none stops the browser from faking a bold or italic that the font file lacks.
  6. The two smoothing rules switch macOS from subpixel to grayscale antialiasing. Text renders thinner and matches the weight shown in Figma. Other systems ignore them.
  7. 100svh is the viewport height with the mobile browser toolbars showing, so the page never sits behind them. 100dvh changes as the toolbar moves and re-lays out the page each time.
  8. scrollbar-gutter: stable reserves the scrollbar's space. The page does not shift sideways when a modal locks scrolling or when content grows past one screen.
  9. overflow-wrap: break-word breaks a word that cannot fit on a line, such as a URL, where it would otherwise run out of its box. Long text that wraps, truncates or clamps has the rest.
  10. -webkit-text-size-adjust: 100% stops iOS from inflating text in landscape.
  11. -webkit-tap-highlight-color: transparent removes the gray flash iOS and Android paint over a tapped element.
  12. Inputs at 16px or more stop iOS Safari from zooming the page on focus. It does not zoom back out.
  13. touch-action: manipulation tells the browser the element never double-tap zooms, so click fires without the wait.
  14. user-select: none on controls stops a long press from selecting a button's label.

Rules

  1. Load every weight and style the design uses. With font-synthesis: none a missing bold renders as regular.
  2. With min-width: 0 on everything, a sized image, icon or avatar in a flex row shrinks to make room for the text beside it. Give it flex: none.
  3. The reset removes the tap highlight, so every pressable element needs its own :active state. Press feedback has it.
  4. Never fix input zoom with user-scalable=no or maximum-scale=1. That takes zoom away from people who need it.
  5. Keep user-select: none to controls. Never set it on body or on links, because people copy text.
  6. Text the user types inherits pretty, in textarea and contenteditable. If lines shift while typing, set text-wrap: stable on that element.
  7. The reserved gutter puts centered content slightly off-center on a page too short to scroll. Chris Coyier documents this. Accept it.
  8. svh is for documents and heroes. An app shell with UI pinned to the bottom uses height: 100dvh, so it tracks the toolbar. Emil Kowalski draws this line in his mobile-native skill.

Support

text-wrap: pretty in Chrome 117 and Safari 26, ignored by Firefox. scrollbar-gutter in Chrome 94, Firefox 97, Safari 18.2. interpolate-size in Chrome 129 only. The rest works everywhere.

Credits

Borrowed from: Travis Arnold, @souporserious on X, 2025-04-02, "2025 CSS Reset" · Ayomidé Daniel, @aydahnizzy on X, 2025-10-10, for the font rules · text-wrap: pretty on everything is a house rule · Kevin Powell, 3 modern CSS properties to add to your reset, 2026-04-02, for svh and the gutter · Emil Kowalski, mobile-native skill, for the touch rules · Chris Coyier, The Downsides of scrollbar-gutter: stable, 2025-12-03 · Ahmad Shadeed, Defensive CSS, for overflow-wrap and the flex: none rule

Background: Jen Simmons, Better typography with text-wrap pretty · Adam Argyle, CSS text-wrap: pretty

Docs: MDN: text-wrap · MDN: font-synthesis · MDN: font-smooth

Entry 02

Logical properties

Use them in place of every left, right, top and bottom, in spacing, borders, offsets and alignment.

Open
The second card is the mirror image of the first: border, text and close button all swap sides.
Code CSS · 13 lines
.card {
  position: relative;
  padding-inline: 1.5rem;
  margin-block-end: 2rem;
  border-inline-start: 4px solid;
  text-align: start;
}

.card .close {
  position: absolute;
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
}
Why it works, the rules and the credits 1 reason · 1 rule · support · credits

Why it works

  1. inline is the direction text runs and block the direction blocks stack. Start and end follow dir and writing-mode, so one set of declarations renders mirrored in a right-to-left page.

Rules

  1. The four-value margin, padding and inset shorthands stay physical. Use the -inline and -block shorthands.

Support

Chrome
87
Firefox
66
Safari
14.1

Credits

Borrowed from: Ahmad Shadeed, Digging Into CSS Logical Properties, 2021-03-10

Entry 03

OKLCH color

Write colors in oklch() and derive every related color from a base with color-mix(in oklch, …). Never hardcode a second hex or an rgba() for a hover, tint or transparent version.

Open
Move the hue. The hover and the subtle color follow, and stay as light as they were.
Code CSS · 5 lines
:root {
  --primary: oklch(55% 0.15 250);
  --primary-hover: color-mix(in oklch, var(--primary), black 15%);
  --primary-subtle: color-mix(in oklch, var(--primary) 12%, transparent);
}
Why it works, the rules and the credits 3 reasons · 1 rule · credits

Why it works

  1. The three channels are lightness, chroma and hue. Lightness is perceptual, so two colors with the same L look equally light whatever their hue. Scales and contrast become predictable.
  2. Mixing in OKLCH keeps the hue of the base color, where sRGB mixing drifts toward gray.
  3. One base value drives the whole family. Change the base and the hover and subtle versions follow.

Rules

  1. Write grays, white and black with none as the hue, as in oklch(98% 0 none). A written hue of 0 is red, and it pulls every mix toward red. Half white oklch(1 0 0) and half a blue at hue 264 came out at hue 312 in Chrome and Safari. With none, or with the keyword white, it stayed at 264.

Credits

Borrowed from: Andrey Sitnik and Travis Turner (Evil Martians), OKLCH in CSS: why we moved from RGB and HSL · Björn Ottosson, A perceptual color space for image processing

Docs: MDN: oklch() · MDN: color-mix() · oklch.com picker

Entry 04

One set of color tokens for light and dark

Use it on any site with a dark mode. Each token holds both values, so there is no second block of variables under a media query or a .dark class.

Open
Switch the theme. The page and its controls flip, and the two fixed sections never change.
Code CSS · 8 lines
:root {
  color-scheme: light dark;
  --background: light-dark(oklch(1 0 none), oklch(0.145 0 none));
  --foreground: light-dark(oklch(0.145 0 none), oklch(0.985 0 none));
}

[data-theme="light"] { color-scheme: light; }
[data-theme="dark"] { color-scheme: dark; }
Why it works, the rules and the credits 3 reasons · 4 rules · support · credits

Why it works

  1. color-scheme: light dark says the page supports both and follows the system setting. It also switches the browser's own UI, such as form controls, scrollbars and the default page background.
  2. light-dark(a, b) returns a in a light scheme and b in a dark one.
  3. It resolves per element. Set color-scheme on any subtree and every token used inside it flips. The same tokens serve a manual toggle and a section that is always dark.

Rules

  1. Switch themes by setting color-scheme. Never redefine the tokens.
  2. The grays use none for the hue, so they can be mixed with a color later without drifting toward red. OKLCH color has the measurement.
  3. light-dark() takes two colors. Anything else that differs by theme, such as an image, needs its own rule.
  4. Add <meta name="color-scheme" content="light dark"> so the browser paints the right background before the CSS loads.

Support

Chrome
123
Firefox
120
Safari
17.5

Baseline since May 2024.

Credits

Borrowed from: IP, @ipwanciu on X, 2025-10-30

Docs: MDN: light-dark() · MDN: color-scheme · Bramus Van Damme, CSS color-scheme-dependent colors with light-dark()

Entry 05

Fluid sizes with clamp()

Use it for anything that should grow with the screen, starting with font sizes and section spacing. One declaration replaces a ladder of breakpoint overrides.

Open
Change the width. The text is 16px up to a 400px viewport and 28px from 1600px. Open it in its own tab for the full range.
Code CSS · 4 lines
:root {
  --text-body: clamp(1rem, 0.75rem + 1vw, 1.75rem);
  --space-section: clamp(3rem, 1rem + 6vw, 8rem);
}
Why it works, the rules and the credits 3 reasons · 6 rules · credits

Why it works

  1. clamp(min, preferred, max) uses the preferred value and stops at the two bounds.
  2. The vw part of the preferred value grows with the viewport. The rem part sets where the growth starts.
  3. --text-body above is 16px up to a 400px viewport, 28px from 1600px, and a straight line between.

Rules

  1. Write the preferred value as rem + vw, never vw alone. clamp(1rem, 1vw, 1.75rem) stays at 16px on every screen narrower than 1600px, because 1vw is smaller than 1rem until then. A bare vw value also ignores the reader's font size setting.
  2. Work the preferred value out from two points. The slope is (max - min) / (wide - narrow), in px over px, and times 100 it is the vw number. The rem part is min - slope × narrow.
  3. Keep both bounds in rem so they follow the reader's font size.
  4. For a font size, keep the maximum at or below 2.5 times the minimum. The vw part does not grow when the reader zooms, so fluid text grows less than the page. Past 2.5 times, 500% zoom cannot double the text at some widths, and that fails WCAG 1.4.4.
  5. Put fluid values in tokens. Components use the token and never repeat the math.
  6. For more than two or three sizes, derive a whole scale as One fluid scale for type and space does.

Credits

Borrowed from: Kyrylo Silin, @kyrylo on X, 2026-05-10 · James Gilyead and Trys Mudford, Utopia · Adrian Roselli, Responsive Type and Zoom · Maxwell Barvian, Addressing Accessibility Concerns With Using Fluid Type, 2023-11-07, for the 2.5 limit

Docs: MDN: clamp() · Adrian Bece, Modern Fluid Typography Using CSS Clamp

Entry 06

One fluid scale for type and space

Use it when a design has more than two or three font sizes. Six numbers describe every size, and the browser derives the steps. It replaces a table of sizes per breakpoint, and it replaces a generator that someone has to find and run again.

Open
Make the frame narrower. Every size shrinks along a straight line, and stops changing below 320px and above 1240px. At 1240px or wider the readout says body 20px, title 48.83px and padding 40px. At 320px it says 18px, 37.32px and 18px.
Code CSS · 35 lines
:root {
  --narrow: 20;
  --wide: 77.5;
  --size-narrow: 1.125;
  --size-wide: 1.25;
  --ratio-narrow: 1.2;
  --ratio-wide: 1.25;

  --fluid: clamp(0rem, (100vw - var(--narrow) * 1rem) / (var(--wide) - var(--narrow)), 1rem);
  --at-narrow: calc(var(--size-narrow) * (1rem - var(--fluid)));
  --at-wide: calc(var(--size-wide) * var(--fluid));

  --step--1: calc(var(--step-0) / var(--ratio-narrow));
  --step-0: calc(var(--at-narrow) + var(--at-wide));
  --step-1: calc(var(--at-narrow) * var(--ratio-narrow) + var(--at-wide) * var(--ratio-wide));
  --step-2: calc(var(--at-narrow) * pow(var(--ratio-narrow), 2) + var(--at-wide) * pow(var(--ratio-wide), 2));
  --step-3: calc(var(--at-narrow) * pow(var(--ratio-narrow), 3) + var(--at-wide) * pow(var(--ratio-wide), 3));
  --step-4: calc(var(--at-narrow) * pow(var(--ratio-narrow), 4) + var(--at-wide) * pow(var(--ratio-wide), 4));

  --space-s: var(--step-0);
  --space-m: calc(1.5 * var(--step-0));
  --space-l: calc(2 * var(--step-0));
  --space-s-l: calc(var(--at-narrow) + 2 * var(--at-wide));
}

body { font-size: var(--step-0); }
h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-2); }
small { font-size: var(--step--1); }

.card {
  display: grid;
  gap: var(--space-s);
  padding: var(--space-s-l);
}
Why it works, the rules and the credits 5 reasons · 7 rules · support · credits

Why it works

  1. The first six lines are the whole design decision. Widths and sizes are in rem, written as plain numbers, so 20 is 320px and 1.125 is 18px. Body text runs from 18px on a 320px screen to 20px on a 1240px one, and each heading step is 1.2 times the last on the phone and 1.25 times on the desktop.
  2. --fluid is a length that is 0 at the narrow width, 1rem at the wide width and a straight line between. It holds the only clamp(), and that one bounds every token.
  3. --at-narrow and --at-wide are the two base sizes, each weighted by how close the viewport is to its width. Their sum is step 0.
  4. pow() raises each ratio to the step number. A step is the narrow scale's size times one weight plus the wide scale's size times the other, which is the line Fluid sizes with clamp() draws between two points.
  5. A space token is a multiple of step 0, so it grows as much as body text does. A pair such as --space-s-l gives the two terms different multipliers. It is the small size on the phone and the large size on the desktop, 18px to 40px here.

Rules

  1. Write the inputs as plain numbers. With 20rem and 77.5rem the --fluid line divides a length by a length, and Firefox does not support that.
  2. Change an input in the rule that declares the steps. A step takes its value where it is declared, so a ratio set on a descendant changes nothing below it.
  3. Derive sizes below step 0 from step 0 and the narrow ratio, as --step--1 does. With both ratios a small size shrinks as the screen grows.
  4. Check the top step against the limit in Fluid sizes with clamp(). Step n passes when (size-wide / size-narrow) × (ratio-wide / ratio-narrow)ⁿ is 2.5 or less. The example gives 1.31 at step 4.
  5. Write a pair only where a layout uses it, and never use one for a font size. A pair is steep on purpose and breaks that limit.
  6. For a scale that follows the container, write 100cqi in place of 100vw. Container queries with container units has the rules.
  7. Components use the tokens and never repeat the math.

Support

Chrome
120
Firefox
118
Safari
15.4

Set by pow(). Tested in Chrome 150 and real Safari 27 on 2026-10-01, where every step came out within 0.001px of the formula.

Credits

Borrowed from: James Gilyead, Designing with fluid type scales, 2020-02-01, for the two scales · Trys Mudford, CSS-only fluid modular type scales, 2020-02-02, and Fluid custom properties, 2020-02-03 · James Gilyead, Designing with a fluid space palette, 2021-03-16, for space pairs. Utopia prints one finished clamp() per step from a calculator, because CSS had no powers in 2020. With pow() the browser does that math.

Background: Miriam Suzanne, Responsive and Fluid Typography with Baseline CSS Features, 2025-12-16

Docs: MDN: pow() · Utopia type calculator