Skip to main content
Technology & EngineeringFile Formats378 lines

Variable Font Technology

Variable fonts — a single font file containing a continuous range of styles (weight, width, slant, optical size) controlled by design axes, replacing entire font families.

Quick Summary18 lines
You are a file format specialist with deep expertise in variable font technology, including OpenType 1.8+ fvar/gvar/STAT tables, design axis configuration, CSS font-variation-settings and high-level properties, fontmake build workflows, instance generation with varLib.instancer, and web font optimization with subsetting.

## Key Points

- **Extensions:** `.ttf`, `.otf`, `.woff2` (same as static fonts, format is backward-compatible)
- **MIME types:** Same as regular fonts
- **Specification:** OpenType 1.8+ (ISO/IEC 14496-22:2019)
- **Outline types:** TrueType (gvar) or CFF2 (both support variation)
- **CSS syntax:** `font-variation-settings` or high-level properties
- Named instances = predefined axis positions (e.g., "Bold" = wght:700)
- Applications can list these as traditional style options
- Light master (wght=300)
- Regular master (wght=400, default)
- Bold master (wght=700)
- **Web typography:** Single WOFF2 replacing 4-12 static font files
- **Responsive design:** Adjust weight/width based on viewport or container
skilldb get file-formats-skills/Variable Font TechnologyFull skill: 378 lines
Paste into your CLAUDE.md or agent config

You are a file format specialist with deep expertise in variable font technology, including OpenType 1.8+ fvar/gvar/STAT tables, design axis configuration, CSS font-variation-settings and high-level properties, fontmake build workflows, instance generation with varLib.instancer, and web font optimization with subsetting.

Variable Font Technology

Overview

Variable fonts are an extension of the OpenType specification (version 1.8, released 2016) that allows a single font file to contain a continuous range of design variations along defined axes. Instead of shipping separate files for Regular, Bold, Light, Condensed, and Italic, a variable font encodes the entire design space in one file with interpolation between master designs.

Developed jointly by Apple, Google, Microsoft, and Adobe, variable fonts dramatically reduce the number of font files needed for web typography while enabling fine-grained typographic control that was previously impossible without custom font files for each weight or width.

Core Philosophy

Variable fonts represent a fundamental shift in typography: instead of shipping separate font files for each weight, width, italic angle, and optical size, a single variable font file contains a continuous design space that can be interpolated to any point along its defined axes. One file replaces what previously required a dozen or more static font files.

The practical benefits are significant: smaller total file sizes (one variable font typically replaces 6-12 static fonts with less total data), the ability to use precise intermediate weights (not just Regular and Bold, but weight 450 or 650), responsive typography that adapts weight and width to viewport size, and smoother animation between typographic states. For web projects, variable fonts reduce HTTP requests and total font download size simultaneously.

Variable fonts use the same OpenType container (.ttf or .otf) and are deployed on the web as WOFF2. Browser support is now universal across modern browsers. Use CSS font-variation-settings or the higher-level font-weight, font-stretch, and font-style properties to control variation axes. When selecting a variable font, evaluate which axes it exposes (weight, width, slant, optical size, and potentially custom axes) to ensure it covers your design needs.

Technical Specifications

  • Extensions: .ttf, .otf, .woff2 (same as static fonts, format is backward-compatible)
  • MIME types: Same as regular fonts
  • Specification: OpenType 1.8+ (ISO/IEC 14496-22:2019)
  • Outline types: TrueType (gvar) or CFF2 (both support variation)
  • CSS syntax: font-variation-settings or high-level properties

Registered Design Axes

TagAxisCSS PropertyRange
wghtWeightfont-weight1-1000 (typically 100-900)
wdthWidthfont-stretch>0% (typically 75-125%)
italItalicfont-style: italic0-1 (binary toggle)
slntSlantfont-style: oblique Xdeg-90 to 90 degrees
opszOptical sizefont-optical-sizingTypically 8-144 pt

Custom axes use uppercase tags (e.g., GRAD for Grade, CASL for Casual).

Key OpenType Tables

[Variation Tables]
  fvar  — Font Variations: defines axes (tag, min, default, max, name)
  gvar  — Glyph Variations: per-glyph deltas for TrueType outlines
  CFF2  — Charstring variations for PostScript outlines
  cvar  — CVT Variations: hinting value adjustments
  HVAR  — Horizontal Metrics Variations
  VVAR  — Vertical Metrics Variations
  MVAR  — Metrics Variations (global metrics like ascender, descender)
  STAT  — Style Attributes: axis value names for UI presentation
  avar  — Axis Variations: non-linear axis mapping (piecewise linear)

[Instance Records in fvar]
  - Named instances = predefined axis positions (e.g., "Bold" = wght:700)
  - Applications can list these as traditional style options

How Variation Works

Master designs are placed at specific points in the design space:
  - Light master (wght=300)
  - Regular master (wght=400, default)
  - Bold master (wght=700)

For any requested weight value, the renderer interpolates:
  wght=550 → blend 50% between Regular(400) and Bold(700)

Each glyph stores deltas (point offsets) from the default master.
The renderer applies: final_point = default_point + Σ(delta_i × scalar_i)
where scalar_i depends on axis position and region influence.

How to Work With It

Using in CSS

/* Basic usage with font-variation-settings */
@font-face {
    font-family: 'Inter';
    src: url('Inter-Variable.woff2') format('woff2');
    font-weight: 100 900;
    font-display: swap;
}

/* Using standard CSS properties (preferred) */
h1 {
    font-family: 'Inter', sans-serif;
    font-weight: 750;          /* any value, not just 400/700 */
    font-stretch: 87.5%;       /* if width axis exists */
    font-style: oblique 12deg; /* if slant axis exists */
    font-optical-sizing: auto; /* if opsz axis exists */
}

/* Using font-variation-settings for custom axes */
.text {
    font-variation-settings:
        "wght" 625,
        "wdth" 90,
        "GRAD" -50,          /* custom Grade axis */
        "CASL" 0.5;          /* custom Casual axis */
}

/* Animation (smooth transitions between any values) */
.heading {
    font-weight: 400;
    transition: font-weight 0.3s ease;
}
.heading:hover {
    font-weight: 800;
}

/* Keyframe animation */
@keyframes breathe {
    0%, 100% { font-variation-settings: "wght" 300; }
    50% { font-variation-settings: "wght" 700; }
}

Inspecting Variable Fonts

# Python with fonttools
from fontTools.ttLib import TTFont

font = TTFont('variable.ttf')
if 'fvar' in font:
    fvar = font['fvar']
    print("Axes:")
    for axis in fvar.axes:
        print(f"  {axis.axisTag}: {axis.minValue} - {axis.maxValue} (default: {axis.defaultValue})")
    print("\nNamed instances:")
    for instance in fvar.instances:
        name = font['name'].getDebugName(instance.subfamilyNameID)
        print(f"  {name}: {instance.coordinates}")
# Command line
fonttools varLib.instancer --help  # instance generation tool

# Web inspection
# https://wakamaifondue.com — drag-and-drop variable font inspector
# https://v-fonts.com — variable font playground

Creating Variable Fonts

# From multiple UFO sources with fontmake
# pip install fontmake
fontmake -m MyFont.designspace -o variable-ttf
fontmake -m MyFont.designspace -o variable-cff2

# The .designspace file defines sources and axes:
<!-- MyFont.designspace -->
<?xml version="1.0" encoding="UTF-8"?>
<designspace format="4.1">
  <axes>
    <axis tag="wght" name="Weight" minimum="300" default="400" maximum="800"/>
  </axes>
  <sources>
    <source filename="MyFont-Light.ufo" familyname="MyFont" stylename="Light">
      <location><dimension name="Weight" xvalue="300"/></location>
    </source>
    <source filename="MyFont-Regular.ufo" familyname="MyFont" stylename="Regular">
      <location><dimension name="Weight" xvalue="400"/></location>
    </source>
    <source filename="MyFont-Bold.ufo" familyname="MyFont" stylename="Bold">
      <location><dimension name="Weight" xvalue="800"/></location>
    </source>
  </sources>
</designspace>

Generating Static Instances from Variable Font

# Extract specific instances
fonttools varLib.instancer variable.ttf wght=700 -o Bold.ttf
fonttools varLib.instancer variable.ttf wght=400 wdth=75 -o CondensedRegular.ttf

# Generate all named instances
fonttools varLib.instancer variable.ttf --named-instances --output-dir=./instances/

Subsetting for Web

# Subset variable font (preserving variation data)
pyftsubset variable.ttf \
    --output-file=variable-latin.woff2 \
    --flavor=woff2 \
    --unicodes="U+0000-00FF" \
    --layout-features="kern,liga,calt" \
    --variations="wght:300:800"  # preserve weight range

Common Use Cases

  • Web typography: Single WOFF2 replacing 4-12 static font files
  • Responsive design: Adjust weight/width based on viewport or container
  • Dark mode: Slightly increase weight for light-on-dark text (optical compensation)
  • Animated typography: Smooth transitions between styles on hover/scroll
  • Accessibility: Users can fine-tune weight and width for readability
  • App UI: One font file for all UI contexts (navigation, body, headings)
  • Branding: Continuous design space for nuanced typographic expression

Pros & Cons

Pros

  • Single file replaces entire font family (4-20+ files)
  • Smaller total download size for websites using multiple weights
  • Continuous design space — any value on any axis, not just predefined styles
  • CSS animation between axis values is smooth and hardware-accelerated
  • Optical size axis automatically adjusts design for different point sizes
  • Named instances provide familiar style names for applications
  • Backward compatible — static font applications see default instance

Cons

  • Single file is larger than any individual static font (30-50% larger than one weight)
  • If only using 1-2 weights, static fonts may be smaller total download
  • Browser/OS support for custom axes varies
  • Some applications show all named instances but not custom axis sliders
  • font-variation-settings is lower-level than high-level CSS properties
  • CFF2 variable fonts have less tool support than TrueType variable fonts
  • Creating variable fonts requires more design effort (masters must be compatible)

Compatibility

PlatformSupportNotes
Chrome66+Full support including animations
Firefox62+Full support
Safari11+Full support (macOS and iOS)
Edge17+Full support
Android8.0+System and web
macOS10.13+System-wide support
Windows10 1709+DirectWrite support
Adobe appsCC 2018+Full support in InDesign, Illustrator, Photoshop

Notable variable fonts: Inter, Recursive, Roboto Flex, Source Sans 3, IBM Plex, Fraunces, Commissioner, Plus Jakarta Sans.

Tools: Wakamaifondue (web inspector), Samsa (web testing), Axis-Praxis (playground), Glyphs/FontLab/RoboFont (creation).

Practical Usage

Optimize a variable font for web delivery with subsetting

# Install fonttools with woff2 support
pip install fonttools brotli

# Subset to Latin characters only, preserving weight axis
pyftsubset InterVariable.ttf \
    --output-file=Inter-Latin.woff2 \
    --flavor=woff2 \
    --unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD" \
    --layout-features="kern,liga,calt,cv01,cv02" \
    --variations="wght:300:800"

# Generate a static instance from a variable font
fonttools varLib.instancer InterVariable.ttf wght=700 -o Inter-Bold.ttf

# Generate all named instances
fonttools varLib.instancer InterVariable.ttf \
    --named-instances --output-dir=./static-instances/

echo "Variable: $(stat -c%s InterVariable.ttf) bytes"
echo "Subset WOFF2: $(stat -c%s Inter-Latin.woff2) bytes"

Implement responsive typography with CSS variable fonts

/* Load variable font with full weight range */
@font-face {
    font-family: 'Inter';
    src: url('Inter-Latin.woff2') format('woff2');
    font-weight: 100 900;
    font-display: swap;
}

/* Responsive weight adjustment based on viewport */
body {
    font-family: 'Inter', system-ui, sans-serif;
    font-weight: 400;
    font-optical-sizing: auto;
}

/* Slightly heavier text for dark mode (optical compensation) */
@media (prefers-color-scheme: dark) {
    body { font-weight: 350; }
    h1   { font-weight: 650; }
}

/* Fluid weight scaling with container queries */
@container (min-width: 600px) {
    .card-title {
        font-weight: 600;
        font-stretch: 110%;
    }
}

/* Smooth weight animation on interaction */
.nav-link {
    font-weight: 400;
    transition: font-weight 0.2s ease, font-stretch 0.2s ease;
}
.nav-link:hover {
    font-weight: 700;
}

Inspect variable font axes and instances with Python

from fontTools.ttLib import TTFont

font = TTFont('MyVariable.ttf')

# Check if font is variable
if 'fvar' not in font:
    print("Not a variable font")
    exit()

fvar = font['fvar']
name_table = font['name']

print("=== Design Axes ===")
for axis in fvar.axes:
    axis_name = name_table.getDebugName(axis.axisNameID)
    print(f"  {axis.axisTag} ({axis_name}): {axis.minValue} - {axis.maxValue} "
          f"(default: {axis.defaultValue})")

print("\n=== Named Instances ===")
for inst in fvar.instances:
    inst_name = name_table.getDebugName(inst.subfamilyNameID)
    coords = ", ".join(f"{k}={v}" for k, v in inst.coordinates.items())
    print(f"  {inst_name}: {coords}")

# Check for STAT table (axis value names)
if 'STAT' in font:
    print("\n=== STAT Axis Values ===")
    for val in font['STAT'].table.AxisValueArray.AxisValue:
        print(f"  {name_table.getDebugName(val.ValueNameID)}")

Anti-Patterns

Using font-variation-settings instead of high-level CSS properties for registered axes. Setting font-variation-settings: "wght" 700 instead of font-weight: 700 bypasses the CSS cascade for font properties, causes inheritance issues, and does not work with font-style: oblique Xdeg or font-optical-sizing: auto. Always use standard CSS properties for registered axes (wght, wdth, ital, slnt, opsz) and reserve font-variation-settings only for custom axes.

Loading a full variable font when only one or two weights are needed. A variable font file is 30-50% larger than a single static weight. If a site only uses Regular (400) and Bold (700), two static WOFF2 files will be smaller than the variable font. Evaluate your actual usage: variable fonts save bandwidth only when using 3+ weights or widths from the same family.

Animating font-variation-settings without considering performance. While CSS transitions on variable font axes are visually smooth, animating font-variation-settings triggers layout recalculation on every frame because text reflows. Limit animations to non-layout contexts (hover effects on short text, decorative headings) and avoid animating body text or long paragraphs.

Forgetting to configure the STAT table, causing incorrect style names in application menus. Without a properly configured STAT (Style Attributes) table, applications like Word, InDesign, and system font pickers will display confusing or duplicate style names. Always populate STAT with axis value entries that map to familiar names (Regular, Bold, Condensed, etc.).

Subsetting a variable font and accidentally removing axis data. Running pyftsubset without the --variations flag strips variation tables, producing a static font at the default instance. Always include --variations="wght:MIN:MAX" (or * to keep all axes) when subsetting variable fonts for web delivery.

Related Formats

  • TTF/OTF — Static font formats that variable fonts extend
  • WOFF2 — Compressed delivery format (variable fonts served as WOFF2)
  • GX Variations — Apple's earlier variation technology (precursor)
  • Multiple Master — Adobe's legacy variation format (Type 1, deprecated)
  • COLR v1 — Color font format that can also be variable
  • designspace — Source format defining variable font axes and masters

Install this skill directly: skilldb add file-formats-skills

Get CLI access →