Replace multiple static font files with a single variable WOFF2. Control weight, width, italic, and optical-size axes with CSS font-variation-settings and animate them for interactive typography.
A variable font packs an entire type family — every weight, every width, every optical size — into a single WOFF2 file using interpolation mathematics. Instead of loading inter-300.woff2, inter-400.woff2, inter-500.woff2, inter-700.woff2, you load one inter-var.woff2 and control everything with CSS. This eliminates 3-7 network requests, enables fluid weight transitions, and opens design possibilities that static fonts simply cannot offer.
A variable font defines a design space with one or more axes. Each axis has a minimum value, a maximum value, and a default. The font contains outline data at the extremes (and sometimes intermediate masters) and uses linear interpolation to render any value in between. The weight axis (wght) might span 100–900; at 400 you get Regular, at 700 you get Bold, but you can also set 423 or 612 — infinite intermediate steps. The italic axis (ital) is binary on many fonts (0=upright, 1=italic) or continuous on others. The width axis (wdth) controls condensed-to-expanded. The optical size axis (opsz) adjusts letterform details for different size contexts — at 12 the strokes are slightly thicker and spacing looser for legibility; at 72 the letterforms are crisper and more refined for display. Custom axes (four uppercase letter tags) let type designers expose proprietary design dimensions — e.g., GRAD (grade) on Roboto Flex, or YOPQ (y opaque) for stroke weight control.
CSS maps the most common variable font axes to familiar high-level properties: font-weight maps to wght, font-style: italic maps to ital, font-stretch maps to wdth, font-optical-sizing: auto maps to opsz. Use these high-level properties whenever possible — they are readable, inheritable, and support the cascade correctly. Use font-variation-settings only for axes that do not have a dedicated CSS property (custom axes, or wdth when you need sub-pixel precision). Important: font-variation-settings does not inherit individual axis values — it replaces the entire string. If a parent sets font-variation-settings: 'wght' 700 and a child adds font-variation-settings: 'GRAD' 50, the child loses the wght 700 setting. The workaround is CSS custom properties.
1/* High-level properties — PREFER these */
2.heading {
3 font-weight: 700; /* maps to wght axis */
4 font-style: italic; /* maps to ital axis */
5 font-stretch: 75%; /* maps to wdth axis (condensed) */
6 font-optical-sizing: auto; /* maps to opsz axis */
7}
8
9/* font-variation-settings — for custom axes only */
10.heading-graded {
11 font-weight: 700; /* Still use high-level for wght */
12 font-variation-settings: 'GRAD' 50; /* Custom grade axis */
13}
14
15/* Workaround for inheritance issue: use custom properties */
16:root {
17 --font-wght: 400;
18 --font-grad: 0;
19}
20.heading {
21 --font-wght: 700;
22 --font-grad: 50;
23 font-variation-settings: 'wght' var(--font-wght), 'GRAD' var(--font-grad);
24}The @font-face declaration for a variable font uses a weight range instead of a single value. Declare font-weight: 100 900 (min max) to signal to the browser that this single file covers the entire weight range. Without this declaration, the browser may load the variable font but refuse to render intermediate weights because it thinks only 400 and 700 are available.
1/* Variable font @font-face: weight range is critical */
2@font-face {
3 font-family: 'Inter';
4 src: url('/fonts/inter-var.woff2') format('woff2');
5 font-weight: 100 900; /* Declares full axis range */
6 font-style: normal;
7 font-display: swap;
8}
9
10/* Separate italic variable font (if the font ships two files) */
11@font-face {
12 font-family: 'Inter';
13 src: url('/fonts/inter-var-italic.woff2') format('woff2');
14 font-weight: 100 900;
15 font-style: italic;
16 font-display: swap;
17}
18
19/* Usage: any weight value now works */
20.fine { font-weight: 100; }
21.regular { font-weight: 400; }
22.medium { font-weight: 500; }
23.bold { font-weight: 700; }
24.black { font-weight: 900; }
25.custom { font-weight: 362; } /* Works — interpolated */Variable font axes are animatable with CSS transitions and animations because they are numeric values. Hover effects where text weight increases on hover, loading spinners where stroke weight pulses, or headings that expand from condensed to normal — all achievable with a single font file and CSS. The performance cost is low: animating font-variation-settings triggers text rasterisation on each frame, which is handled on the compositor thread in modern browsers when the element is composited. Keep animated font elements on their own compositing layer (transform: translateZ(0) or will-change: font-variation-settings) for smoothest results.
1/* Interactive weight hover effect */
2.interactive-heading {
3 font-family: 'Inter', sans-serif;
4 font-weight: 400;
5 transition: font-weight 200ms ease-out;
6}
7
8.interactive-heading:hover {
9 font-weight: 700;
10}
11
12/* CSS keyframe animation on optical size axis */
13@keyframes size-pulse {
14 0% { font-variation-settings: 'opsz' 12; }
15 50% { font-variation-settings: 'opsz' 72; }
16 100% { font-variation-settings: 'opsz' 12; }
17}
18
19.pulsing-text {
20 font-family: 'Roboto Flex', sans-serif;
21 animation: size-pulse 2s ease-in-out infinite;
22}
23
24/* Custom property animation for multiple axes */
25@property --font-weight {
26 syntax: '<number>';
27 initial-value: 400;
28 inherits: false;
29}
30
31.animatable {
32 font-variation-settings: 'wght' var(--font-weight);
33 transition: --font-weight 300ms ease;
34}
35.animatable:hover { --font-weight: 750; }Chrome DevTools and Firefox both include a font inspector that shows all available axes for any variable font on the page. In Chrome: open DevTools, select an element using a variable font, go to the Elements panel, then the Computed tab, click on the font name or open the More Tools > Fonts panel. Firefox has a dedicated Typography panel in its developer tools showing all axes with live sliders. Use this to discover custom axes that aren't documented, verify that your @font-face declarations expose the full range, and experiment with axis values before writing CSS. For programmatic discovery, the CSS Font Loading API exposes axis information.
1// Inspect available axes via the Font Loading API
2document.fonts.ready.then(() => {
3 document.fonts.forEach(font => {
4 console.log(
5 font.family,
6 font.weight,
7 // Note: axis data requires the CSS Font Loading API Level 2
8 // which adds FontFaceDescriptors with variationSettings
9 );
10 });
11});
12
13/* CSS: verify your axis range is correct */
14.test-extremes {
15 /* Test minimum */
16 font-variation-settings: 'wght' 100;
17}
18.test-max {
19 /* Test maximum */
20 font-variation-settings: 'wght' 900;
21}
22/* If these render identically, your @font-face range is wrong */A button that uses variable font animation on hover, demonstrating wght and wdth axis control.
Next lesson covers fluid typography with CSS clamp() — eliminating breakpoint-based font scaling in favour of smooth continuous scaling across all viewport widths.
Your progress is saved in this browser, on this device only.