Web typography has long operated under the constraints of arbitrary browser defaults, platform-specific rendering engines, and a reliance on JavaScript utilities to fix fundamental text layout issues. For years, web developers relied on CSS resets to strip away user-agent quirks, manually adjusted baselines using negative margins, or loaded client-side scripts to prevent stranded single words at the end of paragraphs. Today, a new suite of CSS specifications is transforming how browsers render text, bringing fine-grained control over layout, font metrics, and OpenType features directly into CSS.
To assemble these capabilities into a practical, resilient framework, a modernized stylesheet collection called tidy-typography.css gathers these advancements into a baseline implementation. The stylesheet brings together rendering hints, container-aware fluid sizing, vertical line-box trimming, advanced text wrapping, locale-aware punctuation, and variable font adjustments. By placing experimental properties behind progressive enhancement guardrails, this modern approach demonstrates how web layout can achieve print-like precision while retaining graceful fallbacks across older browser engines.
Rendering and Fallback Sanity
Establishing control over web typography begins at the root level, where browsers apply default rendering behaviors that often differ across operating systems and hardware configurations. Standard user-agent defaults can introduce unwanted text smoothing anomalies, arbitrary font inflation on mobile devices, or synthesized faux-bold styles when a font family lacks genuine bold or italic weights.
html
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-synthesis: none;
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
body
font-size-adjust: from-font;
The declaration font-synthesis: none explicitly prevents browsers from algorithmically generating bold or italic glyph variants when an underlying font family lacks specific weights or styles. Faux italics and synthetic bolding frequently distort letterforms, degrading readability and altering spatial metrics. Meanwhile, properties such as -webkit-font-smoothing: antialiased and text-rendering: optimizeLegibility serve as browser rendering hints. While primarily targeting WebKit and Blink engines, these properties prioritize kerning and custom ligature features over raw rendering speed, though exact visual results remain dependent on operating system font-rendering pipelines.
On mobile devices, automatic font inflation frequently disrupts responsive design systems by artificially scaling body text upon orientation changes. Setting text-size-adjust: none disables this unpredictable scaling, keeping text dimensions anchored strictly to declared CSS values. To manage fallback transitions while custom web fonts load, font-size-adjust: from-font reads the aspect ratio and x-height metric directly from the primary font file. This minimizes optical size jumps and structural layout shifts when a fallback system font is temporarily swapped out for a loaded webfont.
Fluid Typography with Container Query Units
Traditional fluid typography relies heavily on viewport units, such as vw, combined with CSS clamp() functions to smoothly scale font sizes across device screen widths. However, viewport-based fluid scaling operates without awareness of an individual element’s structural context. For example, a heading placed inside a narrow sidebar will scale based on the width of the entire browser window, causing it to render at an excessively large size relative to its layout container.
.article-container
container-type: inline-size;
:root
--step-0: clamp(1rem, 0.9rem + 1cqi, 1.125rem);
--step-3: clamp(2rem, 1.6rem + 3cqi, 2.75rem);
By defining a parent wrapper with container-type: inline-size, the element becomes a container query context. Child elements can then utilize container query inline units (cqi), where 1cqi represents one percent of the container’s inline width rather than the viewport’s total width.
Replacing vw with cqi inside a standard fluid clamp() formula allows a single typography token to scale accurately regardless of placement. The same heading component can adapt fluidly inside a primary article column or a narrow 300-pixel sidebar without requiring component-specific media query overrides.
Vertical Rhythm: Trimming the Half-Leading
In standard CSS line-box calculations, the line-height property adds extra spatial distribution above the cap-height and below the baseline of text, a concept inherited from traditional typesetting known as half-leading. While essential for preventing adjacent text lines from colliding during continuous reading, half-leading makes precise vertical alignment challenging when text needs to sit flush against surrounding structural components, visual assets, or container borders.
:heading, p, blockquote, figcaption, li
text-box-trim: trim-both;
text-box-edge: cap alphabetic;
/* shorthand: text-box: trim-both cap alphabetic; */
The introduction of text-box-trim solves this long-standing alignment challenge by cropping the built-in space above and below the line box. Used alongside text-box-edge: cap alphabetic, the browser calculates layout boundaries directly from the font’s internal metadata—measuring from the top of uppercase letters down to the alphabetic baseline. This eliminates the need for arbitrary negative margins or fragile padding hacks previously used to compensate for font line height.
@supports selector(:heading)
:heading
text-box: trim-both cap alphabetic;
Because specifications evolve ahead of browser adoption, forward-looking features require targeted feature checks. The Selectors Level 5 specification proposes the :heading pseudo-class to automatically match all structural heading elements from h1 through h6 alongside ARIA role declarations matching [role="heading"]. Until broad browser implementation arrives, wrapping these selectors inside an @supports selector() block guarantees that experimental properties are safely evaluated without causing syntax parsing errors in current engines.
Balanced Headlines and Orphan-Free Paragraphs
Standard text-wrapping algorithms treat line breaks purely based on available spatial width, often resulting in unbalanced headlines with single words stranded on a final line. Historically, developers relied on JavaScript libraries to inspect DOM elements and inject non-breaking spaces ( ) to eliminate these typographic orphans. Modern CSS introduces native algorithmic text wrapping designed specifically for varying text lengths.
:heading, blockquote, figcaption
text-wrap: balance;
p, li, dd
text-wrap: pretty;
max-width: 65ch;
[contenteditable="true"]
text-wrap: stable;
Applying text-wrap: balance causes the layout engine to evaluate the full text string across multiple lines, balancing the character count per line to produce visually uniform typographic blocks. Because this algorithm increases layout processing load, it is optimized for short display text, performing best on blocks limited to about six lines.
For long-form body text, text-wrap: pretty targets orphan prevention without spending computational resources to balance the entire paragraph. The layout engine specifically inspects the final lines of a paragraph, automatically adjusting line breaks to ensure a minimum of two words remain on the terminal line.
Conversely, interactive text fields require a different strategy. Applying text-wrap: stable to [contenteditable="true"] containers prevents the browser from recalculating preceding line breaks as a user types, avoiding disorienting visual shifts during active content creation.
Hyphenation, Properly Tuned
Enabling automatic hyphenation helps maintain uniform line lengths, particularly in narrow columns or justified body text. However, turning on hyphens: auto without configuring explicit constraints can lead to typographic flaws, such as hyphens running across consecutive lines or words broken into illegible fragments.
<html lang="en-GB">
p
hyphens: auto;
Automatic hyphenation depends on native browser dictionary lookups, making an explicit HTML lang attribute essential. Without correct language metadata on the root document element, layout engines cannot access proper dictionary hyphenation rules, causing the property to silently fail.
p
hyphens: auto;
hyphenate-limit-chars: 6 3 3; /* word length, min before break, min after break */
hyphenate-character: "‐"; /* use proper hyphen glyph instead of dash */
hyphenate-limit-lines: 2; /* no more than 2 consecutive hyphenated lines */
hyphenate-limit-zone: 8%; /* reduce overall hyphenation frequency */
hyphenate-limit-last: always; /* never hyphenate the last word of a paragraph */
/* Safari legacy equivalents */
-webkit-hyphens: auto;
-webkit-hyphenate-limit-before: 4;
-webkit-hyphenate-limit-after: 3;
-webkit-hyphenate-limit-lines: 2;
pre, code, var, samp, kbd, :heading
hyphens: manual;
-webkit-hyphens: manual;
Fine-tuning hyphenation behavior requires setting explicit parameters across word structures and line relationships. The hyphenate-limit-chars property sets structural limits by taking three values: minimum total word length, minimum characters before a hyphen break, and minimum characters following it. Restricting consecutive hyphenated lines with hyphenate-limit-lines: 2 prevents vertical "ladders" of hyphens down the right margin, while hyphenate-limit-last: always guarantees that the final word of a paragraph remains whole.
Safari and WebKit engines rely on vendor-prefixed properties for line and character limits, making duplicate declarations necessary for cross-browser compatibility. Furthermore, code structures, variables, technical elements, and major display headings must explicitly set hyphens: manual. Breaking monospaced code strings can alter semantic code meaning, while hyphenating display titles degrades visual readability.
Hanging Punctuation and Ink-Aware Underlines
Standard layout boxes align text strictly to letterform bounding boxes, leaving leading quotation marks or subtle punctuation marks slightly inset visually relative to the solid edge of surrounding text characters. Similarly, historical browser text decoration defaults draw flat, unbroken lines through descending letterforms.
blockquote, p
hanging-punctuation: first allow-end last;
a
text-decoration-thickness: from-font;
text-underline-offset: 0.15em;
text-decoration-skip-ink: auto;
The hanging-punctuation property allows specified punctuation glyphs to extend slightly outside the bounding boundary of the text block. Declaring first allows opening quotation marks at the start of a block to hang into the margin, creating an optically flush vertical edge with the text below. The last keyword applies the same behavior to closing punctuation, while allow-end permits terminal punctuation to hang beyond the right margin instead of forcing an unwanted line wrap.
Standard Alignment:
"Typography is an art."
The line starts here.
Hanging Alignment:
"Typography is an art."
The line starts here.
For inline links, typographic underline properties replace generic visual lines with text-aware styling. text-decoration-thickness: from-font instructs the browser to adopt stroke weights defined directly within the font’s internal design rules. Combined with text-underline-offset, which pushes the line away from the baseline, and text-decoration-skip-ink: auto, which automatically breaks the underline wherever letter descenders like "g", "j", or "p" cross the line path, link decorations remain clear without interrupting glyph readability.
Variable Font Fine-Tuning
Variable fonts expand web typography options by delivering entire typeface families within a single file. Beyond standard weight (wght) and slant (slnt) axes, variable typefaces frequently incorporate optical sizing (opsz) and grade (GRAD) controls, allowing real-time adjustment of glyph forms based on context.
body
font-optical-sizing: auto;
font-variant-ligatures: common-ligatures contextual;
font-kerning: normal;
/* Dark mode grade compensation */
:root
--vf-grad: 0;
@media (prefers-color-scheme: dark)
:root --vf-grad: -50;
*
font-variation-settings: "GRAD" var(--vf-grad, 0);
Enabling font-optical-sizing: auto allows the browser to dynamically adjust glyph stroke proportions based on point size, maintaining subtle, delicate strokes at display sizes while widening strokes for legibility at small sizes. Paired with font-variant-ligatures and font-kerning: normal, the layout engine activates designed character combinations and spatial kerning tables built into the font file.
When interfaces switch to dark mode, light text rendered on dark backgrounds visually expands, appearing heavier than identical font weights displayed against light backdrops. Rather than altering structural font weights—which can trigger layout shifts by altering character widths—fonts containing a GRAD (grade) axis can dynamically compensate for high-contrast optical glare. Modifying the custom property --vf-grad: -50 under dark mode media queries subtly reduces optical stroke thickness while leaving character metrics and line wraps completely unaltered.
@font-face
font-family: "Mono";
src: url("/fonts/mono-var.woff2") format("woff2");
size-adjust: 105%; /* tune per-font */
When combining monospaced code blocks alongside standard proportional body copy, font metric mismatches frequently cause uniform point sizes to look visually uneven. The size-adjust descriptor, defined inside @font-face rules, scales the overall glyph metrics of a specific target font family without altering declared standard font-size units in lower-level CSS rules.
Numeric, Emoji, and Script-Aware Text
Numeric presentation varies depending on context. Displaying data in financial spreadsheets requires fixed-width alignment, whereas numbers integrated within running paragraphs should match the optical proportions of surrounding letters.
table, .tabular
font-variant-numeric: tabular-nums lining-nums;
.prose
font-variant-numeric: oldstyle-nums proportional-nums;
.emoji-color
font-variant-emoji: emoji;
.emoji-mono
font-variant-emoji: text;
Applying tabular-nums and lining-nums forces numbers into equal inline widths and uniform cap heights, allowing numerical columns in data tables to line up cleanly. Conversely, setting oldstyle-nums and proportional-nums in narrative body text introduces varying digit heights and proportional widths, allowing numbers to blend seamlessly into continuous paragraphs without disrupting visual rhythm.
Managing emoji rendering introduces additional cross-platform challenges, as default behaviors often vary between platform-native color glyphs and black-and-white symbol fonts. Using font-variant-emoji provides explicit control, enabling developers to declare whether targets render as full-color vector graphics (emoji) or monochrome text characters (text) without relying on system-level fallback choices.
Locale-Aware Quotes and Native Subscripts and Superscripts
Quotation conventions vary significantly across global regions and language systems. English typographers wrap primary quotes in double marks and secondary quotes in single marks, British English frequently reverses this ordering, and French formatting requires double angle quotation marks (guillemets) separated by non-breaking thin spaces.
:where(:lang(en)) > * quotes: '"' '"' "'" "'";
:where(:lang(en-GB)) > * quotes: "'" "'" '"' '"';
:where(:lang(fr)) > * quotes: "«0202F" "0202F»" '"' '"';
q::before content: open-quote;
q::after content: close-quote;
The quotes property defines specific character hierarchies assigned to matching language tags through the :lang() selector. When native <q> inline elements are parsed, pseudo-elements apply the correct glyph pairs using open-quote and close-quote. Wrapping these internationalization rules inside :where() ensures zero specificity weight, allowing individual component classes to override quote behavior without specificity conflict.
@supports (font-variant-position: sub)
:is(sub, .sub)
vertical-align: baseline;
font-size: 100%;
line-height: inherit;
font-variant-position: sub;
@supports (font-variant-position: super)
:is(sup, .sup)
vertical-align: baseline;
font-size: 100%;
line-height: inherit;
font-variant-position: super;
Standard browser defaults for HTML <sub> and <sup> elements rely on synthesized scaling, shrinking standard letterforms and shifting their vertical baseline. This often alters regular line-box leading and distorts character stroke weights. Utilizing font-variant-position instructs the layout engine to draw true, purpose-built OpenType subscript and superscript glyphs directly from the font file. Baseline alignment, line height, and font size are reset to standard baseline definitions, allowing natively designed alternate characters to render cleanly without disturbing continuous leading lines.
A Note on Specificity: :is() and :where()
Modern CSS resets rely on pseudo-classes like :where() and :is() to manage stylesheet specificity. Understanding the structural differences between these functions is essential for building clean, maintainable baseline stylesheets.
:where(h1, h2, h3, h4, h5, h6)
margin-block: 0.5em;
The :where() function operates with zero specificity, meaning any selectors placed within its argument list contribute nothing to the selector’s total weight. This makes it an ideal choice for baseline resets, as developers can easily override its rules elsewhere in a project using a single class name, eliminating the need for complex, high-specificity overrides.
:is(h1, h2, h3, h4, h5, h6), p, blockquote, figcaption, li
text-box: trim-both cap alphabetic;
Conversely, the :is() pseudo-class takes on the specificity weight of its most specific argument. Its primary advantage lies in selector grouping, allowing long lists of elements—such as repetitive heading tags—to be condensed cleanly into single rules without duplicating selector chains across the stylesheet.
The Complete tidy-typography.css Framework
The following complete implementation combines these modern typographic patterns into a cohesive baseline stylesheet designed for progressive enhancement across web applications:
/* ==========================================================================
tidy-typography.css — a modern typography reset
Every rule degrades gracefully in unsupported browsers.
========================================================================== */
html
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-synthesis: none;
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
body
max-width: 800px;
margin: 0 auto;
padding: 1rem;
/* ---- Fluid typography ---- */
.article-container
container-type: inline-size;
:root
--step-0: clamp(1rem, 0.9rem + 1cqi, 1.125rem);
--step-1: clamp(1.25rem, 1.1rem + 1.5cqi, 1.5rem);
--step-2: clamp(1.6rem, 1.35rem + 2cqi, 2rem);
--step-3: clamp(2rem, 1.6rem + 3cqi, 2.75rem);
--step-4: clamp(2.5rem, 1.9rem + 4.5cqi, 4rem);
--vf-grad: 0;
@media (prefers-color-scheme: dark)
:root
--vf-grad: -50;
/* ---- Variable font fine-tuning ---- */
*
font-variation-settings: "GRAD" var(--vf-grad, 0);
body
font-size: var(--step-0);
font-size-adjust: from-font;
line-height: 1.6;
font-optical-sizing: auto;
font-variant-ligatures: common-ligatures contextual;
font-kerning: normal;
/* ---- Text box trim ---- */
:is(h1, h2, h3, h4, h5, h6), p, blockquote, figcaption, li
text-box: trim-both cap alphabetic;
/* ---- Text wrap ---- */
:is(h1, h2, h3, h4, h5, h6), blockquote, figcaption
text-wrap: balance;
font-size: var(--step-3);
line-height: 1.15;
letter-spacing: -0.01em;
h1
font-size: var(--step-4);
p, li, dd
text-wrap: pretty;
max-width: 65ch;
hyphens: auto;
hyphenate-limit-chars: 6 3 3;
hyphenate-limit-lines: 2;
hyphenate-limit-zone: 8%;
hyphenate-limit-last: always;
pre, code, var, samp, kbd, :is(h1, h2, h3, h4, h5, h6)
hyphens: manual;
blockquote, p
hanging-punctuation: first allow-end last;
[contenteditable="true"]
text-wrap: stable;
/* ---- Numeric, emoji, and script-aware text ---- */
table, .tabular
font-variant-numeric: tabular-nums lining-nums;
.prose
font-variant-numeric: oldstyle-nums proportional-nums;
.emoji-color
font-variant-emoji: emoji;
.emoji-mono
font-variant-emoji: text;
/* ---- Locale-aware quotes ---- */
:where(:lang(en)) > *
quotes: '"' '"' "'" "'";
:where(:lang(en-GB)) > *
quotes: "'" "'" '"' '"';
q::before
content: open-quote;
q::after
content: close-quote;
/* ---- Native sub and superscripts ---- */
@supports (font-variant-position: sub)
sub, .sub
vertical-align: baseline;
font-size: 100%;
line-height: inherit;
font-variant-position: sub;
@supports (font-variant-position: super)
sup, .sup
vertical-align: baseline;
font-size: 100%;
line-height: inherit;
font-variant-position: super;
/* ---- Ink-aware underlines ---- */
a
text-decoration-thickness: from-font;
text-underline-offset: 0.15em;
text-decoration-skip-ink: auto;
What Belongs in a Modern Reset
The evolution of modern CSS standards has significantly altered the scope of front-end stylesheets. By delegating complex presentation tasks directly to the browser—such as orphan management via text-wrap, component-relative fluid typography through container query units, and precise spatial alignment via text-box-trim—developers can move away from fragile JavaScript utilities and negative margin hacks.
At the same time, adopting modern CSS standards requires balancing forward-looking features with practical browser support. Because properties like text-wrap: pretty and text-box-trim are designed to