zudo-css

Type to search...

to open search from anywhere

Variable Fonts

CreatedMar 13, 2026UpdatedMar 26, 2026Takeshi Takatsudo

The Problem

Traditional web typography requires loading separate font files for each weight, width, and style combination. A typical project might load regular, bold, italic, and bold-italic variants — four files — just for body text. AI agents commonly generate CSS that references multiple static font weights (300, 400, 500, 600, 700) with separate @font-face declarations, resulting in five or more HTTP requests and significantly larger total download sizes. Variable fonts solve this by packing an entire range of variations into a single file.

The Solution

Variable fonts contain one or more axes of variation — continuous ranges for properties like weight, width, and slant. A single variable font file replaces multiple static files, reducing network requests and enabling smooth transitions between any values along those axes. The CSS font-variation-settings property provides low-level control, while standard CSS properties (font-weight, font-stretch, font-style) now accept ranges and map directly to registered axes.

Registered Axes

Axis tagCSS propertyDescriptionExample range
wghtfont-weightWeight (thin to black)100–900
wdthfont-stretchWidth (condensed to expanded)75%–125%
slntfont-styleSlant angle-12deg–0deg
italfont-styleItalic (binary toggle)0 or 1
opszfont-optical-sizingOptical size adjustments8–144

Code Examples

Basic Variable Font Setup

@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter-Variable.woff2") format("woff2-variations");
  font-weight: 100 900; /* Declare the full weight range */
  font-display: swap;
}

body {
  font-family: "Inter", system-ui, sans-serif;
}

h1 {
  font-weight: 750; /* Any value in the range — not limited to 100-step increments */
}

.light-text {
  font-weight: 350;
}

.bold-text {
  font-weight: 680;
}
Variable Font Weight Range — Fine-grained Weight Control

Using Standard CSS Properties (Preferred)

/* CORRECT: Use standard CSS properties for registered axes */
h1 {
  font-weight: 800;
  font-stretch: 110%;
  font-style: oblique 8deg;
}

/* AVOID: Low-level font-variation-settings for registered axes */
h1 {
  font-variation-settings: "wght" 800, "wdth" 110, "slnt" -8;
}

Standard properties are preferred because they cascade properly, work with inherit and initial, and don’t override each other. With font-variation-settings, setting one axis resets all others to their defaults.

Custom Axes

Custom axes (identified by uppercase tags) require font-variation-settings:

/* GRAD = Grade axis (custom), adjusts stroke weight without changing width */
.dark-bg-text {
  font-variation-settings: "GRAD" 150;
}

/* CASL = Casual axis in Recursive font */
.casual-text {
  font-variation-settings: "CASL" 1;
}

/* Combining custom axes with standard properties */
.display-text {
  font-weight: 700;
  font-variation-settings: "GRAD" 100, "CASL" 0.5;
}

Responsive Weight with Custom Properties

:root {
  --heading-weight: 700;
  --body-weight: 400;
}

@media (max-width: 768px) {
  :root {
    --heading-weight: 600; /* Slightly lighter on small screens for readability */
    --body-weight: 420; /* Slightly heavier for small screen legibility */
  }
}

h1,
h2,
h3 {
  font-weight: var(--heading-weight);
}

body {
  font-weight: var(--body-weight);
}

Animated Font Variations

.hover-weight {
  font-weight: 400;
  transition: font-weight 0.3s ease;
}

.hover-weight:hover {
  font-weight: 700;
}

/* Smooth weight animation — impossible with static fonts */
@keyframes breathe {
  0%,
  100% {
    font-weight: 300;
  }
  50% {
    font-weight: 700;
  }
}

.animated-text {
  animation: breathe 3s ease-in-out infinite;
}

Optical Sizing

/* Automatic optical sizing (on by default when the font supports it) */
body {
  font-optical-sizing: auto;
}

/* Manual control for specific cases */
.small-caption {
  font-size: 0.75rem;
  font-optical-sizing: auto; /* Font adjusts stroke contrast for small size */
}

.display-hero {
  font-size: 4rem;
  font-optical-sizing: auto; /* Font adjusts for large display size */
}

Progressive Enhancement with @supports

/* Fallback: static font files */
@font-face {
  font-family: "MyFont";
  src: url("/fonts/myfont-regular.woff2") format("woff2");
  font-weight: 400;
}

@font-face {
  font-family: "MyFont";
  src: url("/fonts/myfont-bold.woff2") format("woff2");
  font-weight: 700;
}

/* Variable font override for supporting browsers */
@supports (font-variation-settings: normal) {
  @font-face {
    font-family: "MyFont";
    src: url("/fonts/myfont-variable.woff2") format("woff2-variations");
    font-weight: 100 900;
  }
}

Dark Mode Weight Compensation

/* Text on dark backgrounds appears heavier — reduce weight to compensate */
@media (prefers-color-scheme: dark) {
  body {
    font-weight: 350; /* Lighter than the 400 used in light mode */
  }

  h1 {
    font-weight: 650; /* Lighter than the 700 used in light mode */
  }
}

Common AI Mistakes

  • Loading multiple static font files (regular, medium, semibold, bold) instead of a single variable font file, multiplying HTTP requests unnecessarily
  • Using font-variation-settings for registered axes (weight, width, slant) instead of standard CSS properties — this breaks cascading and resets unspecified axes
  • Not declaring the weight range in @font-face (e.g., font-weight: 100 900), causing browsers to only use the default weight
  • Treating variable font weights like static fonts — only using values at 100-step increments (400, 500, 600) when any value in the range is valid
  • Not compensating for text appearing heavier on dark backgrounds — variable fonts make it easy to subtract 30–50 weight units for dark mode
  • Forgetting that font-variation-settings values all reset when you set any one axis — each declaration must include every axis you want to control
  • Using format("woff2") instead of format("woff2-variations") in the @font-face src descriptor, though most modern browsers accept either

When to Use

Variable fonts are ideal for

  • Projects using 3+ weights of the same font family — the single file is typically smaller than multiple static files
  • Designs that need fine-grained weight control (e.g., 350, 450, 550)
  • Animations or transitions involving weight, width, or slant changes
  • Dark mode designs where weight compensation improves readability
  • Responsive designs that adjust weight based on viewport size or context

Stick with static fonts when

  • Only 1-2 weights are needed — a single static file may be smaller than the variable version
  • The chosen typeface is not available as a variable font
  • Legacy browser support is a hard requirement (IE11)

References

Revision History