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 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-settingsvalues all reset when you set any one axis — each declaration must include every axis you want to control - Using
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)