Variable Fonts & OpenType Features
Unlock expressive, performant typography by mastering variable font axes and the OpenType features that refine spacing, numerals, and fine typographic detail.
9 min read
The full lesson
Static font files were a practical compromise — one file per weight, per width, per style. Variable fonts remove that constraint entirely. A single .woff2 file can cover an entire weight range, shift optical size, adjust width, and do it all at values that never existed in static libraries.
Combined with OpenType layout features — the hidden instructions that govern ligatures, figure styles, small caps, and kerning — variable fonts give designers and developers a level of typographic control once reserved for professional typesetting software. This lesson covers how both work and the modern CSS workflow for each.
What a Variable Font Actually Is
A variable font is a single font file that encodes multiple masters and a set of design axes. Think of axes as named sliders — the font designer sets the minimum and maximum; CSS controls where within that range the text sits.
Every variable font includes at least one registered axis. The five standard axes have fixed four-letter tags:
| Axis tag | CSS property | Range (typical) | What it changes |
|---|---|---|---|
wght | font-weight | 100–900 | Stroke weight |
wdth | font-stretch | 50–200 (percent) | Glyph horizontal width |
ital | font-style | 0–1 | True italic vs. roman |
slnt | font-style: oblique Xdeg | -15–0 | Slant angle of upright forms |
opsz | font-optical-size-adjust / font-variation-settings | 8–144 (pt) | Optical-size compensation |
Custom axes use all-uppercase four-letter tags. The font Recursive, for example, exposes MONO (monospace blend) and CRSV (casual/cursive). You can only access custom axes via font-variation-settings.
Declaring a variable font in CSS
@font-face {
font-family: "Inter Variable";
src: url("/fonts/inter-variable.woff2") format("woff2 supports variations"),
url("/fonts/inter-variable.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
The font-weight: 100 900 range declaration tells the browser this file covers the full weight spectrum. Without it, the browser may only expose a single weight.
Once declared, standard CSS properties control the registered axes:
/* Smooth weight interpolation — value 450 has no static equivalent */
h2 {
font-weight: 450;
}
/* Width axis: condensed headings */
.headline-condensed {
font-stretch: 80%;
}
/* Optical size: optimize forms for small text */
.caption {
font-optical-size-adjust: auto; /* or font-variation-settings: "opsz" 11 */
font-size: 0.75rem;
}
Custom axes require font-variation-settings, which uses a low-level override syntax. This property replaces the entire declaration — it does not merge with other axis values. Only set it on the specific element that needs the custom axis, not on body.
/* Custom axis on a specific element only */
.mono-code {
font-variation-settings: "MONO" 1, "wght" 450;
}
Performance: One File to Rule Them All
The main performance win with variable fonts is fewer network requests. Loading Regular, Bold, Italic, and Bold Italic as static files means four round trips. A variable font covering all of those is one file — and often smaller in total than the four static files combined, once compression is applied.
That said, a variable font is not always the smallest option:
- If your design only uses two weights from a wide-axis family, a subsetter like
pyftsubsetor Fonttools targeting just those named instances may produce smaller files. - System UI fonts (SF Pro on Apple, Segoe UI Variable on Windows 11) ship with the OS — zero bytes downloaded.
The modern approach:
- Start with a variable font and
font-display: swapfor critical text. - Subset to the Unicode ranges you actually use (Latin, extended Latin, or a custom glyph list).
- Preload the variable font for above-the-fold text.
<link rel="preload" href="/fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin>
Avoid the outdated pattern of loading four separate static weight files while only using two weights in the design. You pay the latency cost of four files with no design benefit.
Optical Size: the Hidden Axis
The opsz axis is the most impactful axis that designers rarely use consciously. Historically, type was drawn differently at different sizes. At 8pt, stroke contrast decreases, apertures open up, and spacing loosens so text stays legible. At 64pt, the designer can tighten everything for elegance. Static fonts freeze one master and use it at every size.
With opsz, a single variable font like Adobe Source Serif 4 or Apple SF Pro can automatically adapt its drawing to the rendered size:
h1 {
font-size: clamp(2rem, 5vw + 1rem, 4rem);
font-optical-size-adjust: auto; /* matches opsz to the computed font-size */
}
.label {
font-size: 0.6875rem; /* 11px equivalent */
font-optical-size-adjust: auto;
}
font-optical-size-adjust: auto is the browser default in most modern implementations when a font has an opsz axis. Setting it explicitly makes your intent clear and protects against user-agent resets.
OpenType Features: the Invisible Layer of Typographic Quality
OpenType features are instructions baked into the font file. They substitute or reposition glyphs based on context. The browser exposes them via the font-feature-settings CSS property or via higher-level font-variant-* properties.
Prefer the high-level properties first — they are easier to read, more composable, and apply sensible fallbacks:
| Goal | High-level property | Low-level equivalent |
|---|---|---|
| Small capitals | font-variant-caps: small-caps | font-feature-settings: "smcp" 1 |
| Tabular (equal-width) figures | font-variant-numeric: tabular-nums | font-feature-settings: "tnum" 1 |
| Oldstyle figures | font-variant-numeric: oldstyle-nums | font-feature-settings: "onum" 1 |
| Proportional figures | font-variant-numeric: proportional-nums | font-feature-settings: "pnum" 1 |
| Slashed zero | font-variant-numeric: slashed-zero | font-feature-settings: "zero" 1 |
| Ligatures on (default) | font-variant-ligatures: common-ligatures | font-feature-settings: "liga" 1 |
| Ligatures off | font-variant-ligatures: no-common-ligatures | font-feature-settings: "liga" 0 |
| Contextual alternates | font-variant-alternates: contextual | font-feature-settings: "calt" 1 |
| Fractions | font-variant-numeric: diagonal-fractions | font-feature-settings: "frac" 1 |
Figures: the Detail That Defines Data Quality
The most consequential OpenType decision in UI work is figure style. There are four main types:
- Lining figures are the default in most web fonts. They sit on the baseline and rise to cap height. They feel “official” and align visually with capital letters. Use them in headings and anywhere numbers appear next to capitals.
- Oldstyle figures have varying heights and descenders, like lowercase letters. They blend into running body text — a year like “2026” does not loom over the surrounding words. Use them when numbers appear within prose.
- Tabular figures give each numeral an identical advance width. Columns of numbers stay aligned as values change — critical in dashboards, pricing tables, and data displays.
- Proportional figures use natural spacing. They work well for isolated price labels and callout numbers where column alignment is irrelevant.
These combine: font-variant-numeric: oldstyle-nums proportional-nums applies both at once.
/* Data table: lining, tabular */
.data-table td {
font-variant-numeric: lining-nums tabular-nums;
}
/* Body copy with inline years, prices */
article {
font-variant-numeric: oldstyle-nums proportional-nums;
}
/* Prevent zero/O ambiguity in code or IDs */
.serial-number {
font-variant-numeric: slashed-zero;
font-feature-settings: "zero" 1; /* belt and suspenders for older browsers */
}
Ligatures: When to Keep Them and When to Kill Them
Common ligatures (fi, fl, ff) are on by default. They prevent the fi dot from colliding with the f stem in traditional serif faces. In most modern UI typefaces — Inter, Söhne, DM Sans — the ligatures are minor or invisible because the faces are already designed to avoid those collisions.
Discretionary ligatures (dlig) are decorative joins like ct, st, and Th. They are off by default and should stay off for most UI work. They belong to display and editorial contexts, not product interfaces.
Turn common ligatures OFF in:
- Letter-spaced all-caps text (extra spacing distorts the ligature shapes)
- Code blocks (each glyph needs to be individually recognizable)
code, pre {
font-variant-ligatures: no-common-ligatures;
font-feature-settings: "liga" 0, "calt" 0;
}
Small Caps: Structure, Not Decoration
font-variant-caps: small-caps replaces uppercase letters with glyphs drawn at x-height scale. These are proper small capitals drawn by the type designer — not mechanically scaled-down versions of the full caps, which look thin and wrong.
Use genuine small caps for:
- Abbreviations within body text (CEO, HTML, NASA) — full capitals interrupt the text color and feel shouted
- Section labels and run-in heads
- Legal disclaimers and fine print headers
Always check whether the font includes small-cap glyphs. If it does not, the browser will fake them by scaling full caps down — producing the exact visual failure small caps are designed to prevent. Test with browser DevTools: the font-variant-caps computed value stays regardless of whether the glyphs exist.
Do
Use font-variant-numeric: tabular-nums on all data tables, dashboards, and any display where numbers update or compare across rows. Enable font-variant-numeric: oldstyle-nums proportional-nums for running body text where numerals appear within sentences. Turn off ligatures and contextual alternates on code and pre elements. Prefer high-level font-variant-* properties over raw font-feature-settings for composability.
Don't
Do not load four separate static weight files when a single variable font covers the same range — you pay download overhead for every additional file. Do not set font-variation-settings on body or a high-level element if you plan to use any child-level overrides — the cascade will silently reset unrelated axes. Do not enable discretionary ligatures in product UI — they read as editorial decoration and are distracting in dense interfaces. Do not rely on fake small caps from mechanical scaling; verify the font file includes proper smcp glyphs before using font-variant-caps.
Animating Variable Font Axes
Variable font axes are CSS properties. You can transition and animate them like any other CSS value:
@media (prefers-reduced-motion: no-preference) {
.cta-button {
font-weight: 400;
transition: font-weight 150ms ease-out;
}
.cta-button:hover,
.cta-button:focus-visible {
font-weight: 700;
}
}
Weight transitions are compositor-friendly — they do not trigger layout reflow when the font is set up correctly with font-synthesis: none and the layout does not depend on text width. Width-axis (wdth) animations DO affect text reflow. They are expensive and should be used sparingly, only on contained, fixed-width elements.
Always wrap motion inside prefers-reduced-motion: no-preference. Animated text weight can be disorienting for users with vestibular or cognitive sensitivities.
Figma and Variable Fonts in 2026
Figma supports variable font axes natively. When a variable font is active in a design file, the variable font axis panel appears in the text inspector. This means a design token like font-weight: 450 is fully expressible in Figma and can pass to code via Dev Mode without a separate spec.
The design-to-code workflow for variable fonts:
- Install the variable font in Figma (drag a
.woff2or the installed.ttfversion of the variable font into Figma). - Set axis values in the text inspector. Name these as token values in your token plugin (Style Dictionary, Tokens Studio) so
font.weight.heading-secondary = 450becomes a concrete, transferable value. - Export via W3C DTCG format:
"$type": "number", "$value": 450for weight tokens. - On the engineering side, map to
font-weight: 450in CSS. No static font file can render this; the variable font does it exactly as designed.
The outdated handoff pattern — a Figma file using a static font family with only Bold and Regular, handing over a Zeplin spec that names them — loses all the nuance of intermediate weights and forces developers to approximate.
Accessibility Considerations
Variable fonts and OpenType features have an overall positive relationship with accessibility. Heavier weights at smaller sizes improve legibility for users with low vision. Optical size compensation keeps contrast adequate without requiring larger type. Tabular figures help users with cognitive or attention-related differences read data tables without alignment noise.
A few cautions:
- Avoid relying solely on
font-variant-caps: small-capsto distinguish content — screen readers read the underlying text, which may or may not signal the structural role. Pair with semantic HTML. - Animated font weights must respect
prefers-reduced-motionper WCAG 2.2 criterion 2.3.3 (Animation from Interactions, Level AAA) and the more broadly applicable 2.3.1 for seizure risk. - Do not reduce
font-stretchto the point of illegibility — very condensed text (below 75%) at body sizes creates recognition problems for users with dyslexia or low literacy.