tidy-typography.css: A Reset That Lets CSS Do the Typographer's Job
A new reset, tidy-typography.css, replaces JavaScript orphan fixers and negative-margin hacks with container query units, text-box-trim and tuned hyphenation — all degrading gracefully.
- AuthorRebecca Stone
- ChannelInteraction & Visual Craft
- Filed
- Read4 min

- tidy-typography.css ships five fluid type steps sized in cqi units, from 1rem–1.125rem up to 2.5rem–4rem
- text-wrap: balance works best for blocks up to about six lines; text-wrap: pretty replaces JavaScript orphan fixers
- Hyphenation limits cap consecutive hyphenated lines at 2 and set an 8% hyphenation limit-zone
- Dark mode dials the GRAD axis to -50 to compensate for light-on-dark weight gain
- The proposed :heading selector from Selectors Level 5 is kept behind an @supports check because no browser ships it
A new stylesheet called tidy-typography.css moves typography decisions that once required JavaScript or per-font hacks into plain CSS declarations — with every rule written to degrade gracefully when a browser doesn't support it.
The reset covers eight areas: rendering and fallback rules, fluid sizing, vertical rhythm, text wrapping, hyphenation, hanging punctuation, variable font tuning, and OpenType detail. You can adopt it whole or lift a single declaration that solves the irritation in front of you.
What does container-type change for fluid type?
Viewport-based fluid type has a blind spot: it can't see how wide its own element is. A heading in a 300px sidebar scales as if it fills the whole viewport. The fix is one line — container-type: inline-size on a wrapper — which turns it into a container query. Children can then size themselves in cqi units, so the same heading scales correctly in an article column or a narrow sidebar without component-specific media queries:
--step-3: clamp(2rem, 1.6rem + 3cqi, 2.75rem);
The reset ships five type steps, from --step-0 (clamped between 1rem and 1.125rem) up to --step-4 (2.5rem to 4rem).
How does text-box-trim fix vertical rhythm?
Every line box carries half-leading: extra space above the cap-height and below the baseline. It shows up whenever text must align tightly with another element. text-box: trim-both cap alphabetic removes that space, measuring from capital-letter caps to the alphabetic baseline — a metrics-aware replacement for font-specific negative-margin nudges.
The proposed :heading selector from Selectors Level 5 would match h1–h6 and [role="heading"], but it hasn't landed in browsers. The author keeps it behind an @supports selector() check.
Which wrapping rules replace JavaScript?
Three values do the work. text-wrap: balance distributes headline text across lines and works best for blocks up to about six lines. text-wrap: pretty prevents a stranded final word in paragraphs without balancing the whole block. text-wrap: stable stops editable text from reshuffling on every new word — the reason a contenteditable rule sits alongside the others. Both balance and pretty replace JavaScript orphan fixers outright.
Hyphenation gets detailed tuning rather than a bare hyphens: auto, which needs a correct lang attribute to work at all:
hyphenate-limit-chars: 6 3 3— minimum word length and fragment sizes around a breakhyphenate-limit-lines: 2— no more than two consecutive hyphenated lineshyphenate-limit-zone: 8%— less frequent breaks overallhyphenate-limit-last: always— never hyphenate a paragraph's final word- Safari-prefixed equivalents declared alongside
Code, variables and headings stay at hyphens: manual: a broken code word changes meaning, and a hyphenated heading reads as an error.
What do variable fonts and OpenType features add?
font-optical-sizing: auto lets a font use thinner strokes at display sizes and thicker, more legible strokes at small sizes, based on its rendered size. A dark-mode block dials a font's GRAD axis to -50, because light text on dark reads heavier — without changing glyph shape or width the way a real weight change would.
Numeric handling splits by context. Tables get tabular-nums lining-nums so digits stack into clean columns; flowing prose gets oldstyle-nums proportional-nums so digits sit naturally beside lowercase letters. font-variant-emoji makes the colour-versus-monochrome choice explicit instead of leaving it to platform fallback. And font-variant-position swaps synthesized sub and superscripts — browsers just shrink text and shift the baseline — for glyphs actually designed for that position, though most typefaces include proper numerals only, so check before applying broadly.
Locale-aware quotes rules render the native q element correctly per language: double-outside-single for English, reversed for British English, guillemets with a thin space for French. Wrapping them in :where() keeps specificity at zero, so one plain class can override any default — the principle behind the whole reset.
Browser support for text-wrap and text-box-trim will keep shifting, and unsupported declarations are silently ignored — which only helps when the unenhanced typography stands on its own. As the author puts it, a news article, a marketing page and an editable product interface don't ask the same things of their text; what belongs in your reset depends on the font, the content, and the browsers you still support.
Original: drafts.csswg.org
More from Rebecca Stone
Show full bio
News editor covering technology and product news in Product and interaction design, design systems at Interface Wire.
50 articles