# Variable Fonts

> Use when working with variable fonts — choosing between variable and static files, using weight/width/optical-size/slant axes, setting font-variation-settings vs standard properties, animating axes, or deciding whether a variable font actually saves bytes. Also use when a variable font renders wrong or ignores its axes.

- Skill: `jpoindexter/variable-fonts` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jpoindexter/variable-fonts`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jpoindexter/variable-fonts/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: jpoindexter (https://skillmd.com/u/jpoindexter)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jpoindexter/variable-fonts

---


# Variable fonts

One file containing a continuous design space instead of discrete instances.
Powerful and frequently misused — the most common error is reaching for
`font-variation-settings` when a standard CSS property already maps to the axis.

## 1. When to invoke

- Choosing between a variable font and a set of statics.
- Using weight, width, optical size, slant, or italic axes.
- Axes not responding, or rendering incorrectly.
- Animating or transitioning a typographic axis.
- Auditing whether a variable font is actually paying for itself.

## 2. Required context

- **Which axes** the font exposes, and their **ranges** (read the font, don't
  assume — `wght 100–900` is common but not universal).
- **How many instances** you'd otherwise ship.
- **Real byte comparison**: variable file vs. the statics it replaces.
- Whether you need **continuous** values or only named instances.
- Whether the design needs **optical sizing**.

## 3. Invariant principles

- **Use standard CSS properties, not `font-variation-settings`, wherever one
  exists.** Registered axes map to real properties:

  | Axis | Standard property |
  |---|---|
  | `wght` | `font-weight` |
  | `wdth` | `font-stretch` (or `font-width`) |
  | `opsz` | `font-optical-sizing` |
  | `ital` | `font-style: italic` |
  | `slnt` | `font-style: oblique <angle>` |

  `font-variation-settings` is a low-level override. It **does not inherit
  predictably per-axis** — setting it replaces the whole list, so a child that sets
  one axis loses every other. It also bypasses the cascade's understanding of
  weight, breaking things like `font-synthesis` control and browser bolding logic.
  Reserve it for **custom (unregistered) axes only**.

- **Declare the axis range in `@font-face`.** `font-weight: 100 900` on the
  `@font-face` rule tells the browser the file covers that span; omit it and the
  browser may treat the font as a single weight and synthesize faux bold.
- **A variable font is not automatically smaller.** It carries the whole design
  space. Compare real bytes.
- **Only ship the axes you use.** Instancing/subsetting can strip unused axes.

## 4. Context-dependent heuristics

**Variable vs. static — the break-even.**

| Instances needed | Usually |
|---|---|
| 1–2 | Static wins |
| 3 | Roughly even — measure |
| 4+ | Variable usually wins |
| Continuous values or animation | Variable, necessarily |

Measure, don't assume. A variable file replacing two weights often *costs* bytes.

**Optical size (`opsz`) is the underused one.** It adjusts the design for the size
it's rendered at — more contrast and tighter spacing at display sizes, sturdier
forms and looser spacing at text sizes. This is what metal type did by cutting each
size separately. Enable with `font-optical-sizing: auto` (browsers apply it from
`font-size` automatically when the axis exists). Turn it off only deliberately.

**Weight is now continuous.** You can use 450 or 530. Resist it in a design system —
the value of a scale is exclusion. Pick your named steps and tokenize them.

**Grade (`GRAD`) — the axis most people don't know they need.** Grade changes
apparent weight **without changing advance widths**, so text gets visually heavier
or lighter while occupying exactly the same space. Nothing reflows.

Two uses:

- **Dark mode.** Light-on-dark text appears optically bloated — the light strokes
  bleed into the dark ground. Drop the grade slightly in dark mode and the text
  reads at the same visual weight as its light-mode counterpart. (In print the
  relationship inverts.) This is the correct fix for "our dark mode looks heavier",
  far better than switching to a lighter *weight*, which would reflow the line.
- **Interaction states.** Because nothing reflows, grade is safe for hover and
  active states in a way `font-weight` is not.

`GRAD` is a custom axis (Roboto Flex exposes it), so it is one of the legitimate
cases for `font-variation-settings` — there is no standard property for it.

**Parametric axes** go further still, exposing thin-stroke, thick-stroke, counter
width, and similar low-level parameters. Powerful for fine-tuning a face to a
specific context; easy to produce something the type designer would not recognise.
Use sparingly and compare against the designed instances. Custom axes are
arbitrary — a type designer can expose almost any parameter of the design this
way, down to contrast, x-height, serif shape, or the length of a single glyph's
tail (Rutter cites a Q's tail as a real example); the four-character tag is
whatever the foundry chose, so read the specimen rather than guessing at what a
custom axis controls.

**Multiplexed fonts** solve the same reflow problem a different way: every weight
occupies identical horizontal space by design, so switching weight never changes
width. See `ui-component-typography`.

**Animation.** Axes are animatable, which enables genuinely new effects. Constrain
it: animating `wght` reflows text if the width changes, and any motion must respect
`prefers-reduced-motion`.

```css
@media (prefers-reduced-motion: no-preference) {
  .headline { transition: font-variation-settings 200ms ease; }
}
```

**Fallbacks.** Variable font support is universal in current browsers, but the file
still has to load. The metric-matched-fallback technique in
`font-loading-and-performance` applies unchanged.

## 5. Failure patterns

| Pattern | Cause | Fix |
|---|---|---|
| Faux bold despite a variable font | `@font-face` didn't declare the range | `font-weight: 100 900` in `@font-face` |
| Child element loses width/optical settings | `font-variation-settings` replaces the whole list | Use standard properties |
| Variable font larger than the statics it replaced | Replaced too few instances | Measure; consider statics |
| Axis has no effect | Axis not present, or outside its range | Inspect the font's actual axes |
| Text reflows during a weight animation | Weight change alters advance widths | Animate a non-reflowing property, or reserve space |
| Display text looks flabby at 72px | `opsz` unused or disabled | `font-optical-sizing: auto` |
| Design drifts to arbitrary weights | Continuous axis, no tokens | Tokenize named steps |
| Dark mode text looks heavier than light mode | Optical bloat of light-on-dark | Lower `GRAD`, not `wght` — no reflow |
| Hover state shifts the layout | `font-weight` changes advance widths | `GRAD`, or a multiplexed face |
| Parametric tuning produced something odd | Low-level axes pushed past the design space | Compare against the designed instances |
| Motion sickness complaints | Axis animation, no reduced-motion guard | Wrap in the media query |

## 6. Evaluation procedure

1. Inspect the font's **actual axes and ranges** (browser devtools, `fonttools`, or
   the foundry spec). Don't assume.
2. Count the instances the design genuinely uses.
3. **Measure both**: variable file vs. sum of the statics, after subsetting.
4. Confirm `@font-face` declares each axis range.
5. Confirm standard properties are used for registered axes; `font-variation-settings`
   only for custom axes.
6. Check inheritance: set an axis on a parent, verify a child that changes one axis
   retains the rest.
7. If `opsz` exists, verify `font-optical-sizing: auto` and compare display vs. text
   rendering.
8. Confirm no faux bold/italic is being synthesized.
9. If animating, verify `prefers-reduced-motion` is respected and no reflow occurs.

## 7. Output format

```
Font: <name>
Axes available: <wght 100–900, wdth 75–125, opsz 14–72, …>
Axes used: <list> — via <standard property | font-variation-settings + reason>
@font-face ranges declared: <yes/no>
Instances replaced: <n> — variable <n>KB vs statics <n>KB → <win|loss>
Optical sizing: <auto|off> — because <reason>
Tokenized weights: <list>
Faux synthesis: <none|present>
Reduced-motion respected: <yes/n-a>
```

## 8. Examples

**Design system shipping Inter at 400/500/600/700 as four static files.**

> Four instances is past the break-even, and the variable file measures smaller
> than the four statics after subsetting, so switch. Declare
> `font-weight: 100 900` in `@font-face` so the browser knows the span and stops
> synthesizing. Use `font-weight` — not `font-variation-settings` — so the cascade
> and `font-synthesis` keep working. Tokenize exactly the four existing steps
> despite the axis now being continuous; the scale's value is that 530 is
> unavailable.

**Headline set in a family with an `opsz` axis, looking weak at 64px.**

> The optical size axis isn't engaged, so the display setting is using text-optimised
> forms — too little contrast and too much spacing for 64px. `font-optical-sizing:
> auto` lets the browser drive `opsz` from the rendered size; the headline picks up
> the higher-contrast, tighter-spaced cut it was designed to have. No markup change,
> no extra bytes.

## 9. Counterexamples

- ❌ `font-variation-settings: "wght" 700` when `font-weight: 700` exists. — Breaks
  inheritance and cascade behavior.
- ❌ "Variable fonts are always smaller." — They carry the full design space; two
  weights are often cheaper as statics.
- ❌ Omitting `font-weight: 100 900` from `@font-face`. — Invites faux bold.
- ❌ "We have a continuous weight axis, so let designers pick any value." — Destroys
  the system's constraint.
- ❌ Animating `wght` on body text without checking reflow or reduced-motion.
- ❌ "It has an `opsz` axis so it handles sizes automatically." — Only if optical
  sizing is enabled and not overridden.

## 10. Source citations

- **Current standards take precedence here** — variable fonts postdate most of the
  corpus. MDN —
  [variable fonts guide](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_fonts/Variable_fonts_guide),
  [`font-variation-settings`](https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settings),
  [`font-optical-sizing`](https://developer.mozilla.org/en-US/docs/Web/CSS/font-optical-sizing).
  [web.dev — optimize web fonts](https://web.dev/learn/performance/optimize-web-fonts).
- Stocks, *Universal Principles of Typography* (2024) — the most current book in the
  corpus on variable fonts. §29 multiple widths; §30 masters and interpolation
  (weight and width masters combined by vector addition); §31 multiplexed typefaces;
  §84–85 variable fonts and axes; **§86 grade** — "grade, unlike weight (except in
  multiplexed fonts), doesn't affect the overall width of the glyphs", and "on
  screens, light-on-dark text will appear slightly bloated, so, if possible, switch
  to a lighter grade when designing for Dark Mode. The opposite is true in print";
  §87 parametric axes; §88 the inheritance problem; §27 avoid faux styles.
- Brown, *Flexible Typesetting* — designing typographic systems that adapt rather
  than fixed instances; the conceptual case for continuous ranges.
- Rutter, *Web Typography* — OpenType foundations that variable fonts extend;
  custom axes as arbitrary designer-defined parameters (contrast, x-height, serif
  shape, individual glyph details) reachable only through
  `font-variation-settings`. **His browser-support and format-string details
  (`format('woff-variations')`, multiple repeated `@font-face` rules for
  fallback, an early-2018 viability estimate) are from 2017 and superseded —
  variable font support is now universal; don't carry those specifics forward.**

