zudo-css-wisdom
GitHub repository

Type to search...

to open search from anywhere

Variable Fonts

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

CreatedUpdated