opsz: auto vs Manual Optical Sizing
This guide is part of the Optical Sizing & Variable Font Axes in CSS guide, itself part of the Typography Fundamentals & System Architecture area.
Variable fonts with an opsz axis ship two ways to drive it: let the browser derive the value automatically from the rendered font-size with font-optical-sizing: auto, or pin an exact value yourself with font-variation-settings: "opsz" N. Both are legitimate, but they solve different problems, and mixing them without understanding the override order produces a page where headings and body text quietly diverge from the type designer's intent — thin, wiry glyphs at large display sizes, or bloated, over-inked glyphs in dense UI text. This page works through exactly how each mode resolves a value, when to leave the axis on automatic, when to pin it manually, and the specific interaction bug that appears when both are declared on the same rule.
Prerequisites
You need a variable font that actually registers the opsz axis — not every variable font does. Confirm it with fonttools:
fc-query --format='%{axis}\n' YourFont-Variable.ttf
# or, more reliably:
python -m fontTools.varLib.instancer --info YourFont-Variable.ttf
Look for an axis tag of opsz in the output, along with its minValue, defaultValue, and maxValue. Fonts such as Recursive, Roboto Flex, and Source Serif Variable all expose it; many corporate variable fonts only expose wght. If opsz is absent, none of this page applies — you are looking at a static-optical-size font and should read Controlling the opsz Axis in Variable Fonts for background on what the axis does before continuing here. You should also already have the font declared with @font-face and a sane fallback font stack in place, since optical sizing decisions only matter once the font is actually loading and rendering.
Implementation: font-optical-sizing: auto
auto is the initial value of font-optical-sizing in every browser that supports the property, so in practice most pages get this behavior for free the moment they use an opsz-enabled variable font. The browser reads the computed font-size of each element, maps it into the font's opsz range, and requests that instance from the variable font's design space — no extra CSS required:
@font-face {
font-family: "Roboto Flex";
src: url("/fonts/RobotoFlex-Variable.woff2") format("woff2-variations");
font-optical-sizing: auto; /* explicit, though it's the default */
font-weight: 100 1000;
}
body {
font-family: "Roboto Flex", system-ui, sans-serif;
font-size: 1rem; /* opsz resolves near the font's low-size instance */
}
h1 {
font-size: 3.5rem; /* same font-family, opsz resolves near a display instance */
}
With this rule set, body text at 16px and an h1 at 56px render from two different points along the opsz axis without you writing a single font-variation-settings declaration. The 16px paragraph gets sturdier strokes and a taller x-height tuned for small reading sizes; the 56px heading gets the higher-contrast, more refined forms the type designer drew for display use. This is the behavior you want for ordinary responsive typography, including a fluid type scale built with clamp(), because the axis tracks the computed size continuously as the viewport resizes rather than snapping between a fixed set of breakpoints.
The mapping itself is not standardized letter-for-letter — each font's opsz axis has its own minValue/maxValue, and the user agent's mapping from CSS pixels to axis units is an implementation detail, not a guaranteed 1:1 correspondence. What is guaranteed is monotonicity: larger font-size always resolves to an opsz value at least as large as a smaller font-size on the same font, within the axis's clamped range. Above the font's maxValue (commonly 72–144 depending on the family), the instance simply pins to the maximum; there's no interpolation past the registered bounds.
Implementation: manual pinning with font-variation-settings
Sometimes you need optical sizing behavior that does not track font-size — an oversized display headline set at a moderate font-size for layout reasons but that should render with true display-cut glyphs, or a UI badge at 24px that should keep the sturdier text-cut forms rather than the thinner display forms auto would select at that size. For that you override the axis explicitly:
.hero-title {
font-family: "Roboto Flex", system-ui, sans-serif;
font-size: 2.25rem;
font-optical-sizing: none; /* stop auto from also touching opsz */
font-variation-settings: "opsz" 96; /* force the display-cut instance */
}
.dense-badge {
font-family: "Roboto Flex", system-ui, sans-serif;
font-size: 1.5rem;
font-optical-sizing: none;
font-variation-settings: "opsz" 14; /* keep the sturdier text-cut forms */
}
Setting font-optical-sizing: none first matters. If you leave it at auto and also declare font-variation-settings: "opsz" 96", the two can conflict depending on cascade and specificity ordering, and in some engines the explicit "opsz" in font-variation-settings is silently overridden by the auto mapping recomputed from font-size on every reflow. Setting font-optical-sizing: none tells the engine "do not synthesize an opsz value from font-size" so your explicit font-variation-settings entry is the only source of truth for that axis on that element.
Interaction with other axes and the cascade
opsz does not exist in isolation — it shares the variation-settings space with wght, wdth, and any custom axes the font registers, and the CSS cascade treats font-variation-settings as a single atomic value rather than per-axis longhand properties. That has a practical consequence: if a parent rule sets font-variation-settings: "wght" 650 for a bold treatment and a child rule adds font-variation-settings: "opsz" 96 without repeating the weight axis, the child's declaration replaces the entire string rather than merging with it, and the weight silently resets to whatever the font's default axis value is. This is one of the most common bugs teams hit when introducing manual opsz overrides into an existing variable-font system:
/* Wrong: child rule drops the inherited weight override */
.card-title {
font-variation-settings: "wght" 650;
}
.card-title .badge {
font-variation-settings: "opsz" 96; /* wght silently reverts to default */
}
/* Right: repeat every axis you need in the same declaration */
.card-title .badge {
font-variation-settings: "wght" 650, "opsz" 96;
}
The same rule applies in the other direction: switching an element from font-optical-sizing: auto to a manual value does not preserve whatever weight or width axis values auto happened to be coexisting with — auto only ever touches opsz, so weight and width are unaffected either way, but any manual font-variation-settings string you write must be complete for every axis you care about on that element, every time you write it.
When the font has a narrow opsz range
Not every variable font invests the same design effort across its opsz range. Some fonts register the axis mostly for API compatibility, with a minValue of 6 and maxValue of 18 and comparatively little visual difference across that span; others, like Roboto Flex, span 8 to 144 with dramatic differences between the text and display cuts. Before deciding a manual override is worth the added specificity and maintenance cost, render a few sample instances at the extremes of the registered range and compare them side by side — fonttools varLib.instancer can generate static test instances at specific axis coordinates for exactly this kind of visual audit:
fonttools varLib.instancer RobotoFlex-Variable.ttf opsz=8 -o /tmp/opsz-8.ttf
fonttools varLib.instancer RobotoFlex-Variable.ttf opsz=144 -o /tmp/opsz-144.ttf
If the two instances look nearly identical, auto is doing all the work you need and a manual override buys nothing but extra CSS to maintain. If they diverge sharply — thinner strokes, tighter spacing, different contrast at the low end — that's the signal a manual pin is worth the specificity for art-directed components that must not drift with font-size.
Verification
Confirm the resolved axis value with DevTools rather than trusting the CSS alone — the font's registered opsz range clamps whatever you request. In Chrome DevTools, select the element, open the Computed panel, and expand font-variation-settings; for elements using auto, switch to the Rendering tab and enable Emulate a focused page, then use the Font Editor overlay (accessible from the inline font-variation-settings badge next to a matched CSS rule) which shows live sliders for every registered axis, including the resolved opsz value the browser actually applied:
// Confirm at runtime which opsz value an element resolved to
const el = document.querySelector(".hero-title");
const cs = getComputedStyle(el);
console.log(cs.fontOpticalSizing, cs.fontVariationSettings);
getComputedStyle reports the declared font-variation-settings string, not necessarily the clamped value the font actually renders if your requested number falls outside the axis's minValue/maxValue — for that, cross-check against the font's registered range with fontTools.varLib.instancer --info from the Prerequisites step. If your CSS requests "opsz" 200 on a font whose maxValue is 144, the rendered glyph pins at 144 even though DevTools echoes back 200 from the declaration.
Common Pitfalls
- Declaring both
autoand a manualopszon the same rule. Withoutfont-optical-sizing: none, engines vary on whether the explicitfont-variation-settingsaxis wins over the size-derived one, producing inconsistent rendering across browsers. Always pair a manual override withfont-optical-sizing: none. - Assuming
opszvalues transfer between fonts. Anopszof 24 means something different for every font family — it is not a normalized 0–1 scale. Copying a magic number from one variable font's CSS into a project using a different font produces the wrong optical cut or clamps silently at the boundary. - Animating
opszon scroll or hover without checking cost. Optical sizing reflows glyph outlines, which is heavier than interpolatingwghtalone in some rasterizers; see Animating Font Weight with Variable Fonts for the general animation-cost tradeoffs that also apply here. - Forgetting the fallback stack doesn't carry
opsz. System fallback fonts have no optical-size axis, so during afont-display: swapgap the fallback renders with none of this nuance; that's a metrics problem, not anopszproblem, and belongs with your accessible fallback font stack design. - Leaving
autoon dense UI components that need a fixed look. Buttons, badges, and table cells that resize slightly across breakpoints will silently shift optical cut underauto; if the design calls for one consistent glyph shape regardless of size, pin it manually instead.
Frequently Asked Questions
Does font-optical-sizing: auto cost extra network bytes?
No. Both auto and a manual opsz value are interpolated from the same variable font file already on the wire; optical sizing changes which point in the existing design space is rendered, it does not fetch anything new. The cost, if any, is a marginally heavier glyph-outline computation per paint, not a network request.
Can I use auto for body text and pin a manual value only for the hero heading?
Yes, and that's a common, sensible split. Leave the default auto on body and most components, and add a single scoped override — font-optical-sizing: none plus a fixed font-variation-settings: "opsz" N" — only on the specific selector that needs an art-directed exception, such as .hero-title in the example above.
Why does my manually pinned opsz look identical to auto at the same font-size?
If the font-size you tested at happens to map to nearly the same axis position auto would have chosen anyway, the two will look the same — that's expected, not a bug. The divergence only becomes visible when the manual value is set far from what auto would pick at that particular size, for example forcing a high opsz on small text or a low opsz on a large heading.
Does opsz also change font-weight, or are they independent axes?
They're independent registered axes, but many type families intentionally correlate their optical cuts with a slight weight or contrast shift as opsz increases, since that's part of what "optical sizing" traditionally means in metal and digital type design. Check the specific font's specimen — some Roboto Flex-style superfamilies expose opsz and wght as fully orthogonal axes, so tuning one won't move the other numerically even if the type designer's optical program subtly correlates them visually.