What Finally Became Safe in Web Typography

Metric overrides, optical sizing, text wrap and font loading. What each feature closed, where support is still uneven, and a correct font face block to copy.

Date

/

Category

Web design

Web design

/

Writer

Keon Hwui Kim

Keon Hwui Kim

In 2013 I shipped a site where the headline arrived, sat there for eight hundred milliseconds in Georgia, then jumped two lines shorter as the real face loaded and every paragraph below it moved. The client saw it, said nothing, and I spent a week writing a font loader in JavaScript that preloaded, set a class on the html element, cached a flag in localStorage, and got the second page view right. That was the standard practice. It was a hundred lines of script to solve a problem that is now three CSS descriptors.

A set of typographic risks that used to require workarounds have quietly closed. Not all of them, and not evenly across browsers, so it is worth going through each one and saying precisely what it does, where it is defined and what its support actually looks like.

font-display, or choosing which failure you want

font-display is a descriptor in @font-face, defined in CSS Fonts Module Level 4. It does not make fonts load faster. It gives you control over the three periods the browser passes through while waiting, and lets you say what should happen in each.

The three periods are the block period, during which an unloaded font renders invisible fallback text; the swap period, during which it renders visible fallback text; and the failure period, after which the browser gives up and uses the fallback permanently.

Value

Block period

Swap period

What you are choosing

auto

User agent defined

User agent defined

Whatever the browser prefers

block

Short

Infinite

Invisible text now, correct type eventually

swap

Extremely small

Infinite

Readable text now, a swap whenever it lands

fallback

Extremely small

Short

Readable text now, a swap only if it is quick

optional

Extremely small

None

No swap at all on this page view

The interesting value is optional, which is the one most people never reach for. It tells the browser to use the web font only if it is already available, and otherwise to render the fallback and never swap. Katie Hempenius and Barry Pollard, in web.dev's font best practices, recommend it outright for performance: it gives minimal delay in rendering text and no font-swap layout shift at all.

The trade is honest and stark. With optional, a slow first visit sees your fallback for the entire session. Whether that is acceptable is a brand question, not a technical one, which is why the default advice of swap persists. But note what swap actually commits you to: a guaranteed reflow at an unpredictable moment, on every uncached visit, forever.

Support is not a consideration. font-display landed in Chrome 60, Firefox 58 and Safari 11.1, and MDN records it as widely available since January 2020.

The metric overrides, and the catch nobody mentions

This is the genuinely new capability, and the reason the JavaScript font loader is dead. CSS Fonts Module Level 5 defines four descriptors that let you reshape a fallback font so that it occupies the same space as the web font it stands in for.

size-adjust is defined in the spec as a descriptor that "defines a multiplier for glyph outlines and metrics associated with this font, to allow the author to harmonize the designs of various fonts when rendered at the same font-size". It scales everything, including glyph advances and baseline tables, which is what fixes horizontal fit.

Alongside it, ascent-override, descent-override and line-gap-override each "specify the ascent metric", "the descent metric" and "the line gap metric" of the font respectively, as a percentage of the effective font size after adjustment. Those three fix vertical fit, which is what determines line box height and therefore whether the paragraph below moves.

Katie Hempenius's Chrome article on font fallbacks explains the division of labour exactly: "By itself, size-adjust has limited applications for improving font fallbacks", but "combining size-adjust with font metric overrides makes it possible to make any two fonts match each other both horizontally and vertically." Get all four right and the swap is invisible. As the article puts it, the approach "can effectively eliminate font-related layout-shifts".

Here is the catch, and it is the reason I would not describe this technique as safe without qualification.

As of early 2026, Safari does not support ascent-override, descent-override or line-gap-override at all. Chrome has had them since version 87 and Firefox since 89. In Safari they are present only in Technology Preview.

I had assumed these were settled, because they have been in Chromium since 2020 and every tutorial treats them as ordinary CSS. MDN's browser compatibility data lists Safari as unsupported, and caniuse shows the descriptors unsupported across the whole stable Safari range, with support only in Technology Preview. Global coverage sits around 81 per cent, not the 95-plus that the phrase "widely available" implies.

size-adjust itself is in better shape: Chrome 92, Firefox 92, Safari 17, and MDN records it as widely available since September 2023. So on Safari you can correct the horizontal fit and not the vertical. That is a partial fix, which is better than nothing and worse than the articles suggest.

Optical sizing, and why opsz is unlike every other axis

font-optical-sizing, defined in CSS Fonts Level 4, takes auto or none. With auto, which is the initial value, the browser adjusts glyph shapes to the rendered size: thicker strokes and larger serifs at small sizes, finer strokes and more stroke contrast at large ones. It works by driving the opsz variation axis of a variable font from the computed font size. MDN records it as widely available since March 2020.

What makes opsz worth a paragraph of its own is that it is the only registered axis with a defined external meaning. Microsoft's OpenType registered axis documentation states that "The scale for the Optical size axis is designed to provide a much stronger degree of interoperability. For instance, two different fonts with an optical-size value of 20 are assumed to be best suited to text set at 20 points, because the scale is designed that way."

Weight 400 in one family is not weight 400 in another. Optical size 20 is supposed to mean the same thing everywhere, which is why the browser can set it automatically and why you almost never need to touch it. The correct action for optical sizing, in nearly every case, is to leave it alone and let auto do its work, which is only true because the axis was designed to be interoperable.

Balance and pretty

Line breaking was the last thing designers were still doing by hand, inserting non-breaking spaces into headlines to stop a one-word last line. text-wrap, from CSS Text Module Level 4, took that over.

balance evens the number of characters across lines, which is what you want for headings, pull quotes and captions. pretty uses a slower algorithm that favours better layout over speed and, in Chromium, concentrates on avoiding orphans at the end of a paragraph.

Two caveats that are easy to miss. First, balance is capped for cost: MDN records it as applying only to blocks of six lines or fewer in Chromium and ten or fewer in Firefox. It is a headline tool, not a body-copy tool, by design. Second, pretty is not one behaviour. WebKit's Safari 26.0 release notes say that "Our implementation of pretty adjusts how text wraps in an effort to even out the ragged edge, improve hyphenation, and prevent short last lines", and, pointedly, that "In WebKit, all lines of text in an element are improved by pretty, not just a select group of lines at the end of the paragraph."

So a paragraph set with text-wrap: pretty will rag differently in Safari than in Chrome. Both are conformant. Neither is wrong. If your layout depends on a specific rag, this property is not the tool.

Value

Chrome and Edge

Firefox

Safari

text-wrap

114

121

17.4

text-wrap: balance

114

121

17.5

text-wrap: pretty

117

Not supported

26

text-wrap-style longhand

130

124

17.5

Both values fail silently and safely. An engine that does not understand them wraps normally, which is the behaviour every page had until 2023.

Variable fonts were not new in 2016

Variable fonts were announced on 14 September 2016, as part of OpenType 1.8, jointly by Google, Microsoft, Apple and Adobe. Google's announcement put the promise simply: "With variable fonts, your device can display text in myriads of weights, widths, or other stylistic variations from a single font file with less space and bandwidth."

The part that is usually left out of that story is that the mechanism was not invented then. Microsoft's own OpenType specification is explicit: "Font variation mechanisms for fonts using TrueType outlines were first introduced by Apple in 'TrueType GX'. Some of the tables used for OpenType Font Variations have been adapted from Apple's earlier specifications with some enhancements and revisions." What happened in 2016 was that four companies who had each tried this separately agreed on one format. The technology is roughly two decades older than the announcement.

For typography work the practical point is support, and support stopped being a question some time ago: Chrome 66, Edge 17, Firefox 62, Safari 11, at around 96.5 per cent of global usage. The only rule worth repeating is MDN's, which reflects W3C intent: the low-level font-variation-settings syntax should be used "only being used to set values or axes not available otherwise". Use font-weight: 375, not font-variation-settings: "wght" 375. The high-level properties inherit, animate and cascade correctly. The low-level one sets every axis at once and quietly resets the ones you left out.

A block I would actually ship

/* The web font. WOFF2 only: it compresses about 30% better than WOFF,
   and nothing that matters lacks it. */
@font-face {
  font-family: "Subject";
  src: url("/fonts/subject.woff2") format("woff2-variations");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

/* The metric-matched fallback. The four numbers are derived from the
   two fonts' own metrics, not guessed. Safari currently honours
   size-adjust here and ignores the three override descriptors. */
@font-face {
  font-family: "Subject fallback";
  src: local("Arial");
  size-adjust: 96.4%;
  ascent-override: 98.2%;
  descent-override: 24.5%;
  line-gap-override: 0%;
}

body {
  font-family: "Subject", "Subject fallback", Arial, sans-serif;
  font-optical-sizing: auto;
}

h1, h2, blockquote {
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}
/* The web font. WOFF2 only: it compresses about 30% better than WOFF,
   and nothing that matters lacks it. */
@font-face {
  font-family: "Subject";
  src: url("/fonts/subject.woff2") format("woff2-variations");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

/* The metric-matched fallback. The four numbers are derived from the
   two fonts' own metrics, not guessed. Safari currently honours
   size-adjust here and ignores the three override descriptors. */
@font-face {
  font-family: "Subject fallback";
  src: local("Arial");
  size-adjust: 96.4%;
  ascent-override: 98.2%;
  descent-override: 24.5%;
  line-gap-override: 0%;
}

body {
  font-family: "Subject", "Subject fallback", Arial, sans-serif;
  font-optical-sizing: auto;
}

h1, h2, blockquote {
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}
/* The web font. WOFF2 only: it compresses about 30% better than WOFF,
   and nothing that matters lacks it. */
@font-face {
  font-family: "Subject";
  src: url("/fonts/subject.woff2") format("woff2-variations");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

/* The metric-matched fallback. The four numbers are derived from the
   two fonts' own metrics, not guessed. Safari currently honours
   size-adjust here and ignores the three override descriptors. */
@font-face {
  font-family: "Subject fallback";
  src: local("Arial");
  size-adjust: 96.4%;
  ascent-override: 98.2%;
  descent-override: 24.5%;
  line-gap-override: 0%;
}

body {
  font-family: "Subject", "Subject fallback", Arial, sans-serif;
  font-optical-sizing: auto;
}

h1, h2, blockquote {
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}

The percentages above are placeholders for a real pair of fonts. They have to be computed from the actual metrics of both faces, and the shape of that calculation is what the Chrome article demonstrates with its Poppins and Arial example, where the four values come out as a size-adjust of 60.85 per cent, an ascent-override of 164.34 per cent, a descent-override of 57.52 per cent and a line-gap-override of 16.43 per cent. Those numbers look absurd until you realise they are compensating for two typefaces with very different relationships between em square and x-height.

Feature

Problem it closed

Support position

font-display

No control over invisible or swapped text during load

Settled since 2020 in all engines

size-adjust

Fallback and web font differ in horizontal fit

Chrome 92, Firefox 92, Safari 17

ascent-override, descent-override, line-gap-override

Fallback and web font differ in line box height, causing reflow

Chrome 87, Firefox 89, absent from stable Safari

font-optical-sizing

One outline used at every size

Widely available since 2020

text-wrap: balance

Manual non-breaking spaces in headlines

Chrome 114, Firefox 121, Safari 17.5

text-wrap: pretty

Orphans and poor rag in body copy

Chrome 117, Safari 26, no Firefox, engines differ

Variable fonts

One file per weight and width

Universal in practice since 2018

What is still not safe

Three things, stated plainly, so that nobody reads the above as an all-clear.

Vertical metric matching is not cross-browser. The override descriptors are the core of the layout shift fix and Safari does not have them in a stable release. If a large part of your audience is on Safari, budget for some residual shift or choose font-display: optional and accept the fallback on cold visits.

text-wrap: pretty is a family of behaviours, not one. Chromium and WebKit have said in their own documentation that they optimise different parts of the paragraph. Treat it as a quality improvement whose exact result you do not control.

And the oldest one, which no descriptor fixes: you still have to look at it. Every feature here adjusts numbers derived from font metadata, and font metadata is frequently wrong. Metric overrides computed from a face with an inflated ascender will match the wrong thing very precisely. The block above is a starting point that a person has to check on a real page, at real sizes, on a real device, which is the same thing that was true in 2013.

Sources

Related reading

More from the Moc Mallen Studio journal