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 tag | CSS property | Description | Example range |
|---|---|---|---|
wght | font-weight | Weight (thin to black) | 100–900 |
wdth | font-stretch | Width (condensed to expanded) | 75%–125% |
slnt | font-style | Slant angle | -12deg–0deg |
ital | font-style | Italic (binary toggle) | 0 or 1 |
opsz | font-optical-sizing | Optical size adjustments | 8–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;
}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-settingsfor registered axes (weight, width, slant) instead of standard CSS properties — this breaks cascading and resets unspecified axesNot declaring the weight range in
@font-face(e.g.,font-weight: 100 900), causing browsers to only use the default weightTreating 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-settingsvalues all reset when you set any one axis — each declaration must include every axis you want to controlUsing
format("woff2")instead offormat("woff2-variations")in the@font-facesrcdescriptor, 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)