Variable Typography is a design aesthetic built on the OpenType variable font specification, where typefaces contain an entire range of stylistic variations – weight, width, slant, optical size, and custom designer-defined axes – within a single font file. Rather than treating text as a static visual element, Variable Typography embraces type as a dynamic, responsive, interactive medium that shifts and animates in response to viewport size, user input, scroll position, and time. The aesthetic draws from kinetic typography traditions in film title design, motion graphics, and experimental type foundries, but recontextualizes them for the web using CSS font-variation-settings, custom properties, and keyframe animations.
At its philosophical core, Variable Typography rejects the notion that a typeface is a fixed artifact. Instead, it treats the design space between a font’s minimum and maximum axis values as a continuous, explorable landscape. A heading might smoothly interpolate from Thin to Black weight as the user scrolls. A navigation label might narrow its width on smaller viewports not through a jarring font swap, but through a fluid wdth axis transition. Interactive elements respond to hover by shifting optical size or custom expressiveness axes, creating a living, breathing typographic surface that feels organic rather than mechanical.
The aesthetic is deeply connected to the work of type foundries like Google Fonts (Roboto Flex with its 12 axes), Dinamo (with ABC Diatype Variable), and Etcetera Type Company, as well as the broader movement toward parametric and generative typography. It is equally at home in editorial long-form reading – where optical size adjustments improve legibility across breakpoints – and in experimental brand sites where animated weight and width create dramatic visual impact. The color palette tends toward restrained, high-contrast schemes that keep the focus on typographic expression, with neutral backgrounds, sharp monochromatic accents, and selective use of color to highlight interactive type behaviors. Layout is typography-first: generous whitespace, strong vertical rhythm, and deliberate scale contrast let the variable type itself carry the visual weight of the design.
wght axis, creating smooth, organic emphasis shifts on hover, scroll, or time-based triggerswdth axis compresses or expands letterforms fluidly to fit containers, replacing abrupt font-size changes with graceful typographic reflowopsz axis automatically adjusts stroke contrast and detail level for different display sizes – thicker strokes at small sizes, refined detail at large sizesfont-variation-settings to create looping, breathing, or wave-like motion across multiple charactersnth-child selectors or JavaScript splitting libraries, creating cascading typographic wavesclamp(), calc(), and container queries adjusting axes across breakpointsCASL for casualness in Recursive, GRAD for grade in Roboto Flex) add personality dimensions beyond standard weight/width/slantVariable Typography favors restrained, high-contrast palettes that place maximum visual emphasis on the type itself. Dark-on-light and light-on-dark schemes dominate, with selective accent color used sparingly to highlight interactive states, axis change indicators, or editorial punctuation. The palette avoids competing with the dynamism of the typography.
| Swatch | Hex | Role/Usage |
|---|---|---|
| Ink Black | #0D0D0D |
Primary dark background, maximum contrast for white type |
| Charcoal | #1A1A2E |
Secondary dark surface, card backgrounds, code blocks |
| Graphite | #2D2D3A |
Tertiary dark surface, subtle elevation layers |
| Slate Mid | #5A5A72 |
Muted body text on light backgrounds, secondary labels |
| Cool Gray | #9090A8 |
Placeholder text, disabled states, axis value labels |
| Silver Wire | #C8C8D8 |
Borders, dividers, subtle UI chrome on dark backgrounds |
| Paper White | #F5F5F0 |
Primary light background, clean editorial surface |
| Pure White | #FFFFFF |
Display text on dark, maximum contrast headlines |
| Signal Blue | #2962FF |
Primary interactive accent, links, active axis indicators |
| Electric Violet | #7C3AED |
Secondary accent, animation highlights, gradient endpoints |
| Kinetic Coral | #FF6B6B |
Warning states, attention markers, hover emphasis |
| Axis Green | #10B981 |
Success states, active toggles, axis range indicators |
| Warm Amber | #F59E0B |
Tertiary accent, weight axis highlight, caution states |
| Deep Navy | #0F172A |
Alternative dark background, editorial depth |
| Whisper Gray | #E8E8EC |
Light mode card surfaces, subtle backgrounds |
:root {
/* Backgrounds */
--vt-bg-dark: #0d0d0d;
--vt-bg-charcoal: #1a1a2e;
--vt-bg-graphite: #2d2d3a;
--vt-bg-light: #f5f5f0;
--vt-bg-whisper: #e8e8ec;
--vt-bg-navy: #0f172a;
/* Text */
--vt-text-primary: #ffffff;
--vt-text-secondary: #c8c8d8;
--vt-text-muted: #9090a8;
--vt-text-dark: #0d0d0d;
--vt-text-body: #5a5a72;
/* Accents */
--vt-accent-blue: #2962ff;
--vt-accent-violet: #7c3aed;
--vt-accent-coral: #ff6b6b;
--vt-accent-green: #10b981;
--vt-accent-amber: #f59e0b;
/* Borders & Chrome */
--vt-border: #c8c8d8;
--vt-border-subtle: rgba(200, 200, 216, 0.15);
--vt-border-focus: var(--vt-accent-blue);
/* Surfaces */
--vt-surface-glass: rgba(255, 255, 255, 0.04);
--vt-surface-elevated: rgba(255, 255, 255, 0.06);
--vt-surface-hover: rgba(41, 98, 255, 0.08);
/* Shadows */
--vt-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
--vt-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.2);
--vt-shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.3);
--vt-shadow-glow-blue: 0 0 20px rgba(41, 98, 255, 0.15);
--vt-shadow-glow-violet: 0 0 20px rgba(124, 58, 237, 0.15);
/* Transitions */
--vt-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--vt-transition-base: 300ms cubic-bezier(0.4, 0, 0.2, 1);
--vt-transition-slow: 600ms cubic-bezier(0.4, 0, 0.2, 1);
--vt-transition-font: 400ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
/* Font axis defaults */
--vt-wght-light: 300;
--vt-wght-regular: 400;
--vt-wght-medium: 500;
--vt-wght-bold: 700;
--vt-wght-black: 900;
--vt-wdth-condensed: 75;
--vt-wdth-normal: 100;
--vt-wdth-expanded: 125;
}
Variable Typography is defined by its font choices. The aesthetic requires variable fonts with multiple axes – ideally weight (wght) and width (wdth) at minimum, with optical size (opsz), grade (GRAD), and custom axes as bonuses. All recommended fonts below are available as variable fonts on Google Fonts.
| Font | Axes | Style | Usage |
|---|---|---|---|
| Inter | wght (100-900), slnt (-10-0) | Neutral, precise humanist sans-serif with 2600+ glyphs | Body text, UI elements, all-purpose workhorse |
| Roboto Flex | wght, wdth, opsz, GRAD, XTRA, YOPQ, YTAS, YTDE, YTFI, YTLC, YTUC, slnt | Google’s most axis-rich variable font with 12 parametric axes | Display headlines, experimental axis demos, responsive systems |
| Recursive | wght (300-1000), CASL (0-1), slnt (-15-0), MONO (0-1), CRSV (0-1) | 5-axis sans/mono with casual-to-linear range | Code blocks, UI with personality, casual-to-formal transitions |
| Outfit | wght (100-900) | Clean geometric sans-serif | Subheadings, navigation, secondary text |
| Fraunces | wght (100-900), opsz (9-144), SOFT (0-100), WONK (0-1) | Old-style serif with “softness” and “wonkiness” custom axes | Editorial headlines, expressive display text |
| DM Sans | wght (100-1000), opsz (14-auto) | Low-contrast geometric sans with optical size axis | Body text, clean UI, accessible reading |
| Source Serif 4 | wght (200-900), opsz (8-60) | Adobe’s editorial serif with optical sizing | Long-form body text, editorial layouts |
| Commissioner | wght (100-900), FLAR (0-100), VOLM (0-100) | Sans-serif with custom “flair” and “volume” axes | Experimental display, interactive axis demos |
| Heading | Body | Mood |
|---|---|---|
| Fraunces 800 (SOFT 50) | Inter 400 | Expressive editorial serif meets precise Swiss sans |
| Roboto Flex 700 (wdth 120) | DM Sans 400 | Dynamic expanded display with clean geometric body |
| Recursive 800 (CASL 1) | Recursive 400 (CASL 0) | Same family casual-to-linear shift for unified personality |
| Commissioner 700 (FLAR 80) | Inter 400 | Decorative flair headline with neutral readable body |
| Outfit 700 | Source Serif 4 400 (opsz 14) | Modern geometric heading with classic editorial body |
@import url('https://fonts.googleapis.com/css2?family=Inter:slnt,wght@-10..0,100..900&family=Roboto+Flex:opsz,wdth,wght@8..144,25..151,100..1000&family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap');
body {
font-family: 'Inter', system-ui, sans-serif;
font-weight: 400;
font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
line-height: 1.7;
letter-spacing: -0.01em;
color: var(--vt-text-secondary);
font-optical-sizing: auto;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1.1;
color: var(--vt-text-primary);
font-variation-settings: 'opsz' 48, 'SOFT' 30, 'WONK' 1;
}
h1 {
font-size: clamp(3rem, 2rem + 5vw, 7rem);
font-weight: 900;
font-variation-settings: 'opsz' 144, 'SOFT' 0, 'WONK' 1;
}
/* Variable weight animation on display text */
.vt-display {
font-family: 'Roboto Flex', sans-serif;
font-size: clamp(4rem, 3rem + 6vw, 9rem);
font-weight: 100;
line-height: 0.95;
letter-spacing: -0.04em;
animation: breathe-weight 4s ease-in-out infinite alternate;
}
@keyframes breathe-weight {
from { font-variation-settings: 'wght' 100, 'wdth' 100; }
to { font-variation-settings: 'wght' 900, 'wdth' 125; }
}
/* Monospace code with Recursive */
code, pre {
font-family: 'Recursive', 'Fira Code', monospace;
font-variation-settings: 'MONO' 1, 'CASL' 0, 'wght' 400;
font-size: 0.9em;
}
/* Casual-to-linear interactive label */
.vt-interactive-label {
font-family: 'Recursive', sans-serif;
font-variation-settings: 'CASL' 0, 'wght' 400;
transition: font-variation-settings var(--vt-transition-font);
}
.vt-interactive-label:hover {
font-variation-settings: 'CASL' 1, 'wght' 700;
}
wdth) tightens on narrow viewports while weight axis (wght) compensates by increasing slightly, maintaining typographic color across breakpoints.vt-card {
background: var(--vt-surface-glass);
border: 1px solid var(--vt-border-subtle);
border-radius: 12px;
padding: 2rem 2.5rem;
position: relative;
overflow: hidden;
transition: border-color var(--vt-transition-base),
box-shadow var(--vt-transition-base);
}
.vt-card::before {
content: '';
position: absolute;
top: 0;
left: 10%;
right: 10%;
height: 1px;
background: linear-gradient(
90deg,
transparent,
var(--vt-accent-blue),
var(--vt-accent-violet),
transparent
);
opacity: 0.4;
transition: opacity var(--vt-transition-base);
}
.vt-card:hover {
border-color: rgba(41, 98, 255, 0.25);
box-shadow: var(--vt-shadow-glow-blue);
}
.vt-card:hover::before {
opacity: 0.8;
}
/* Card title with weight shift on hover */
.vt-card__title {
font-family: 'Inter', sans-serif;
font-size: 1.25rem;
font-variation-settings: 'wght' 500;
letter-spacing: -0.02em;
color: var(--vt-text-primary);
margin-bottom: 0.75rem;
transition: font-variation-settings var(--vt-transition-font);
}
.vt-card:hover .vt-card__title {
font-variation-settings: 'wght' 800;
}
/* Card body text */
.vt-card__body {
font-family: 'Inter', sans-serif;
font-variation-settings: 'wght' 380;
font-size: 0.95rem;
line-height: 1.65;
color: var(--vt-text-muted);
}
/* Primary button with weight-shift hover */
.vt-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.8rem 2rem;
border-radius: 8px;
border: 1px solid var(--vt-accent-blue);
background: var(--vt-accent-blue);
color: #ffffff;
font-family: 'Inter', sans-serif;
font-size: 0.875rem;
font-variation-settings: 'wght' 500;
letter-spacing: 0.01em;
text-decoration: none;
cursor: pointer;
transition: all var(--vt-transition-base),
font-variation-settings var(--vt-transition-font);
}
.vt-button:hover {
font-variation-settings: 'wght' 700;
background: #1a4fd9;
box-shadow: var(--vt-shadow-glow-blue);
transform: translateY(-1px);
}
.vt-button:active {
font-variation-settings: 'wght' 800;
transform: translateY(0);
}
/* Ghost variant */
.vt-button--ghost {
background: transparent;
color: var(--vt-accent-blue);
}
.vt-button--ghost:hover {
background: var(--vt-surface-hover);
box-shadow: none;
}
/* Large display variant with width animation */
.vt-button--display {
font-family: 'Roboto Flex', sans-serif;
font-size: 1.1rem;
padding: 1rem 2.5rem;
font-variation-settings: 'wght' 400, 'wdth' 100;
}
.vt-button--display:hover {
font-variation-settings: 'wght' 700, 'wdth' 115;
}
.vt-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
background: rgba(13, 13, 13, 0.92);
backdrop-filter: blur(16px) saturate(1.2);
-webkit-backdrop-filter: blur(16px) saturate(1.2);
border-bottom: 1px solid var(--vt-border-subtle);
position: sticky;
top: 0;
z-index: 100;
}
.vt-nav__logo {
font-family: 'Roboto Flex', sans-serif;
font-size: 1.25rem;
font-variation-settings: 'wght' 800, 'wdth' 110;
letter-spacing: -0.03em;
color: var(--vt-text-primary);
text-decoration: none;
transition: font-variation-settings var(--vt-transition-font);
}
.vt-nav__logo:hover {
font-variation-settings: 'wght' 900, 'wdth' 125;
}
.vt-nav__links {
display: flex;
align-items: center;
gap: 2rem;
list-style: none;
}
.vt-nav__link {
font-family: 'Inter', sans-serif;
font-size: 0.875rem;
font-variation-settings: 'wght' 400;
letter-spacing: 0.01em;
color: var(--vt-text-muted);
text-decoration: none;
padding: 0.25rem 0;
position: relative;
transition: color var(--vt-transition-fast),
font-variation-settings var(--vt-transition-font);
}
.vt-nav__link:hover {
color: var(--vt-text-primary);
font-variation-settings: 'wght' 700;
}
.vt-nav__link--active {
color: var(--vt-accent-blue);
font-variation-settings: 'wght' 600;
}
/* Animated underline */
.vt-nav__link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: var(--vt-accent-blue);
transition: width var(--vt-transition-base);
}
.vt-nav__link:hover::after,
.vt-nav__link--active::after {
width: 100%;
}
.vt-hero {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 4rem 2rem;
position: relative;
overflow: hidden;
}
.vt-hero__title {
font-family: 'Roboto Flex', sans-serif;
font-size: clamp(4rem, 3rem + 8vw, 12rem);
line-height: 0.9;
letter-spacing: -0.05em;
color: var(--vt-text-primary);
font-variation-settings: 'wght' 100, 'wdth' 100;
animation: hero-breathe 6s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite alternate;
}
@keyframes hero-breathe {
0% { font-variation-settings: 'wght' 100, 'wdth' 75; }
25% { font-variation-settings: 'wght' 400, 'wdth' 100; }
50% { font-variation-settings: 'wght' 700, 'wdth' 115; }
75% { font-variation-settings: 'wght' 1000, 'wdth' 125; }
100% { font-variation-settings: 'wght' 700, 'wdth' 100; }
}
.vt-hero__subtitle {
font-family: 'Inter', sans-serif;
font-size: clamp(1rem, 0.8rem + 1vw, 1.5rem);
font-variation-settings: 'wght' 300;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--vt-text-muted);
margin-top: 2rem;
}
/* Per-character staggered animation */
.vt-hero__title span {
display: inline-block;
animation: char-wave 3s ease-in-out infinite;
}
@keyframes char-wave {
0%, 100% { font-variation-settings: 'wght' 100, 'wdth' 100; }
50% { font-variation-settings: 'wght' 900, 'wdth' 120; }
}
/* Stagger each character */
.vt-hero__title span:nth-child(1) { animation-delay: 0.0s; }
.vt-hero__title span:nth-child(2) { animation-delay: 0.1s; }
.vt-hero__title span:nth-child(3) { animation-delay: 0.2s; }
.vt-hero__title span:nth-child(4) { animation-delay: 0.3s; }
.vt-hero__title span:nth-child(5) { animation-delay: 0.4s; }
.vt-hero__title span:nth-child(6) { animation-delay: 0.5s; }
.vt-hero__title span:nth-child(7) { animation-delay: 0.6s; }
.vt-hero__title span:nth-child(8) { animation-delay: 0.7s; }
.vt-hero__title span:nth-child(9) { animation-delay: 0.8s; }
.vt-hero__title span:nth-child(10) { animation-delay: 0.9s; }
.vt-hero__title span:nth-child(11) { animation-delay: 1.0s; }
.vt-hero__title span:nth-child(12) { animation-delay: 1.1s; }
/* Continuous weight wave across a line of text */
.vt-wave-text {
font-family: 'Roboto Flex', sans-serif;
font-size: clamp(2rem, 1.5rem + 3vw, 5rem);
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--vt-text-primary);
}
.vt-wave-text span {
display: inline-block;
animation: weight-wave 2.5s ease-in-out infinite;
}
@keyframes weight-wave {
0%, 100% { font-variation-settings: 'wght' 200; }
50% { font-variation-settings: 'wght' 900; }
}
/* Generate staggered delays for up to 20 characters */
.vt-wave-text span:nth-child(1) { animation-delay: calc(0 * 0.08s); }
.vt-wave-text span:nth-child(2) { animation-delay: calc(1 * 0.08s); }
.vt-wave-text span:nth-child(3) { animation-delay: calc(2 * 0.08s); }
.vt-wave-text span:nth-child(4) { animation-delay: calc(3 * 0.08s); }
.vt-wave-text span:nth-child(5) { animation-delay: calc(4 * 0.08s); }
.vt-wave-text span:nth-child(6) { animation-delay: calc(5 * 0.08s); }
.vt-wave-text span:nth-child(7) { animation-delay: calc(6 * 0.08s); }
.vt-wave-text span:nth-child(8) { animation-delay: calc(7 * 0.08s); }
.vt-wave-text span:nth-child(9) { animation-delay: calc(8 * 0.08s); }
.vt-wave-text span:nth-child(10) { animation-delay: calc(9 * 0.08s); }
.vt-wave-text span:nth-child(11) { animation-delay: calc(10 * 0.08s); }
.vt-wave-text span:nth-child(12) { animation-delay: calc(11 * 0.08s); }
.vt-wave-text span:nth-child(13) { animation-delay: calc(12 * 0.08s); }
.vt-wave-text span:nth-child(14) { animation-delay: calc(13 * 0.08s); }
.vt-wave-text span:nth-child(15) { animation-delay: calc(14 * 0.08s); }
.vt-wave-text span:nth-child(16) { animation-delay: calc(15 * 0.08s); }
.vt-wave-text span:nth-child(17) { animation-delay: calc(16 * 0.08s); }
.vt-wave-text span:nth-child(18) { animation-delay: calc(17 * 0.08s); }
.vt-wave-text span:nth-child(19) { animation-delay: calc(18 * 0.08s); }
.vt-wave-text span:nth-child(20) { animation-delay: calc(19 * 0.08s); }
/* Section where type weight maps to scroll progress */
.vt-scroll-section {
min-height: 200vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.vt-scroll-section__text {
font-family: 'Roboto Flex', sans-serif;
font-size: clamp(3rem, 2rem + 6vw, 8rem);
line-height: 1.0;
letter-spacing: -0.04em;
color: var(--vt-text-primary);
position: sticky;
top: 50%;
transform: translateY(-50%);
/* font-weight animated via JS scroll handler */
font-variation-settings: 'wght' var(--scroll-weight, 100), 'wdth' var(--scroll-width, 75);
transition: font-variation-settings 50ms linear;
}
/* JavaScript scroll handler (inline in template):
Maps scroll position to CSS custom properties
--scroll-weight: 100 to 900
--scroll-width: 75 to 125
*/
/* UI panel for interactive axis exploration */
.vt-axis-panel {
background: var(--vt-bg-charcoal);
border: 1px solid var(--vt-border-subtle);
border-radius: 12px;
padding: 2rem;
display: grid;
gap: 1.25rem;
}
.vt-axis-panel__label {
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'Recursive', monospace;
font-size: 0.8rem;
font-variation-settings: 'MONO' 1, 'CASL' 0, 'wght' 500;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--vt-text-muted);
}
.vt-axis-panel__value {
font-family: 'Recursive', monospace;
font-variation-settings: 'MONO' 1, 'CASL' 0, 'wght' 400;
color: var(--vt-accent-blue);
}
/* Custom range slider */
.vt-axis-panel__slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
background: var(--vt-bg-graphite);
border-radius: 2px;
outline: none;
}
.vt-axis-panel__slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
background: var(--vt-accent-blue);
border-radius: 50%;
cursor: pointer;
border: 2px solid var(--vt-bg-charcoal);
box-shadow: 0 0 8px rgba(41, 98, 255, 0.3);
transition: box-shadow var(--vt-transition-fast);
}
.vt-axis-panel__slider::-webkit-slider-thumb:hover {
box-shadow: 0 0 16px rgba(41, 98, 255, 0.5);
}
.vt-axis-panel__preview {
font-family: 'Roboto Flex', sans-serif;
font-size: clamp(2rem, 1.5rem + 3vw, 4rem);
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--vt-text-primary);
text-align: center;
padding: 1.5rem 0;
border-top: 1px solid var(--vt-border-subtle);
/* Axis values set via JS from slider inputs */
}
/* Adjust variable font axes across breakpoints */
/* Base (mobile-first) */
.vt-responsive-text {
font-family: 'Roboto Flex', sans-serif;
font-variation-settings: 'wght' 500, 'wdth' 85, 'opsz' 14;
font-size: 1rem;
line-height: 1.65;
}
/* Tablet */
@media (min-width: 640px) {
.vt-responsive-text {
font-variation-settings: 'wght' 420, 'wdth' 95, 'opsz' 18;
font-size: 1.05rem;
line-height: 1.7;
}
}
/* Desktop */
@media (min-width: 1024px) {
.vt-responsive-text {
font-variation-settings: 'wght' 380, 'wdth' 100, 'opsz' 24;
font-size: 1.1rem;
line-height: 1.75;
}
}
/* Large display */
@media (min-width: 1440px) {
.vt-responsive-text {
font-variation-settings: 'wght' 360, 'wdth' 105, 'opsz' 32;
font-size: 1.15rem;
line-height: 1.8;
}
}
/* User preference: reduce motion */
@media (prefers-reduced-motion: reduce) {
.vt-hero__title,
.vt-hero__title span,
.vt-wave-text span,
.vt-display,
.vt-nav__logo,
.vt-button,
.vt-card__title {
animation: none !important;
transition: color var(--vt-transition-fast),
background var(--vt-transition-fast) !important;
}
}
prefers-reduced-motion by disabling or dramatically reducing all font-variation-settings animations for users who request itfont-optical-sizing: auto on body text to let the browser intelligently adjust optical size across different font-size valuesfont-variation-settings transitions can cause jank if too many elements animate simultaneouslyfont-family stack; use @supports (font-variation-settings: normal) to gate variable-font-specific stylesfont-variation-settings for properties that have dedicated CSS equivalents; prefer font-weight: 600 over font-variation-settings: 'wght' 600 for registered axes when not animatingwdth axis rangeCASL slider, label it “Casualness” not “CASL”| Aesthetic | Relationship to Variable Typography |
|---|---|
| Kinetic Typography | Direct ancestor; Variable Typography applies kinetic type principles (motion, timing, expression) specifically through variable font axis manipulation rather than positional animation |
| Swiss/International Style | Shares the typography-first ethos and grid discipline; Variable Typography extends Swiss rationalism with fluid, parametric axis control |
| Editorial Magazine Layout | Both prioritize typographic hierarchy and scale contrast; Variable Typography adds dynamic responsiveness to the editorial tradition of carefully set type |
| Brutalist Web Design | Both strip away decoration to foreground raw structural elements; Brutalism uses fixed, aggressive type while Variable Typography uses fluid, animated type |
| Minimalism | Shares restraint in color and decoration; Variable Typography channels minimalism’s “less is more” into “less decoration, more typographic dynamism” |
| Generative Design | Both treat design elements as parametric, code-driven outputs; generative typography is a subset where variable font axes serve as the parameters |
| Motion Design | Overlapping concern with timing, easing, and choreography; Motion Design uses positional/transform animation while Variable Typography uses axis interpolation |
| Responsive Web Design | Variable Typography extends responsive principles beyond layout reflow into the letterforms themselves, adjusting width and optical size per breakpoint |
| Neubrutalism | Both foreground bold, oversized type with restrained palettes; Neubrutalism uses static heavy weight while Variable Typography animates across weight ranges |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Variable Typography</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:slnt,wght@-10..0,100..900&family=Roboto+Flex:opsz,wdth,wght@8..144,25..151,100..1000&family=Fraunces:opsz,wght@9..144,100..900&family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap" rel="stylesheet">
<style>
/* ========================================
CSS Custom Properties
======================================== */
:root {
--vt-bg-dark: #0d0d0d;
--vt-bg-charcoal: #1a1a2e;
--vt-bg-graphite: #2d2d3a;
--vt-bg-light: #f5f5f0;
--vt-bg-whisper: #e8e8ec;
--vt-bg-navy: #0f172a;
--vt-text-primary: #ffffff;
--vt-text-secondary: #c8c8d8;
--vt-text-muted: #9090a8;
--vt-text-dark: #0d0d0d;
--vt-text-body: #5a5a72;
--vt-accent-blue: #2962ff;
--vt-accent-violet: #7c3aed;
--vt-accent-coral: #ff6b6b;
--vt-accent-green: #10b981;
--vt-accent-amber: #f59e0b;
--vt-border-subtle: rgba(200, 200, 216, 0.12);
--vt-border-focus: var(--vt-accent-blue);
--vt-surface-glass: rgba(255, 255, 255, 0.04);
--vt-surface-hover: rgba(41, 98, 255, 0.08);
--vt-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
--vt-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.2);
--vt-shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.3);
--vt-shadow-glow-blue: 0 0 20px rgba(41, 98, 255, 0.15);
--vt-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--vt-transition-base: 300ms cubic-bezier(0.4, 0, 0.2, 1);
--vt-transition-font: 400ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* ========================================
Reset & Base
======================================== */
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
scroll-snap-type: y proximity;
}
body {
background: var(--vt-bg-dark);
color: var(--vt-text-secondary);
font-family: 'Inter', system-ui, sans-serif;
font-weight: 400;
font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
line-height: 1.7;
letter-spacing: -0.01em;
font-optical-sizing: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
a {
color: var(--vt-accent-blue);
text-decoration: none;
transition: color var(--vt-transition-fast);
}
a:hover {
color: var(--vt-accent-violet);
}
/* ========================================
Navigation
======================================== */
.nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
background: rgba(13, 13, 13, 0.92);
backdrop-filter: blur(16px) saturate(1.2);
-webkit-backdrop-filter: blur(16px) saturate(1.2);
border-bottom: 1px solid var(--vt-border-subtle);
position: sticky;
top: 0;
z-index: 100;
}
.nav__logo {
font-family: 'Roboto Flex', sans-serif;
font-size: 1.2rem;
font-variation-settings: 'wght' 800, 'wdth' 110;
letter-spacing: -0.03em;
color: var(--vt-text-primary);
text-decoration: none;
transition: font-variation-settings var(--vt-transition-font);
}
.nav__logo:hover {
font-variation-settings: 'wght' 900, 'wdth' 130;
color: var(--vt-text-primary);
}
.nav__links {
display: flex;
align-items: center;
gap: 2rem;
list-style: none;
}
.nav__link {
font-family: 'Inter', sans-serif;
font-size: 0.85rem;
font-variation-settings: 'wght' 400;
color: var(--vt-text-muted);
text-decoration: none;
padding: 0.25rem 0;
position: relative;
transition: color var(--vt-transition-fast),
font-variation-settings var(--vt-transition-font);
}
.nav__link:hover {
color: var(--vt-text-primary);
font-variation-settings: 'wght' 700;
}
.nav__link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: var(--vt-accent-blue);
transition: width var(--vt-transition-base);
}
.nav__link:hover::after {
width: 100%;
}
/* ========================================
Hero Section
======================================== */
.hero {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 6rem 2rem;
scroll-snap-align: start;
position: relative;
}
.hero__eyebrow {
font-family: 'Recursive', monospace;
font-size: 0.8rem;
font-variation-settings: 'MONO' 1, 'CASL' 0, 'wght' 500;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--vt-accent-blue);
margin-bottom: 2rem;
}
.hero__title {
font-family: 'Roboto Flex', sans-serif;
font-size: clamp(3.5rem, 2rem + 8vw, 11rem);
line-height: 0.9;
letter-spacing: -0.05em;
color: var(--vt-text-primary);
}
.hero__title span {
display: inline-block;
animation: char-breathe 4s ease-in-out infinite;
}
@keyframes char-breathe {
0%, 100% { font-variation-settings: 'wght' 100, 'wdth' 80; }
50% { font-variation-settings: 'wght' 900, 'wdth' 120; }
}
.hero__title span:nth-child(1) { animation-delay: 0.00s; }
.hero__title span:nth-child(2) { animation-delay: 0.08s; }
.hero__title span:nth-child(3) { animation-delay: 0.16s; }
.hero__title span:nth-child(4) { animation-delay: 0.24s; }
.hero__title span:nth-child(5) { animation-delay: 0.32s; }
.hero__title span:nth-child(6) { animation-delay: 0.40s; }
.hero__title span:nth-child(7) { animation-delay: 0.48s; }
.hero__title span:nth-child(8) { animation-delay: 0.56s; }
.hero__title span:nth-child(9) { animation-delay: 0.64s; }
.hero__title span:nth-child(10) { animation-delay: 0.72s; }
.hero__title span:nth-child(11) { animation-delay: 0.80s; }
.hero__title span:nth-child(12) { animation-delay: 0.88s; }
.hero__title span:nth-child(13) { animation-delay: 0.96s; }
.hero__title span:nth-child(14) { animation-delay: 1.04s; }
.hero__title span:nth-child(15) { animation-delay: 1.12s; }
.hero__title span:nth-child(16) { animation-delay: 1.20s; }
.hero__title span:nth-child(17) { animation-delay: 1.28s; }
.hero__title span:nth-child(18) { animation-delay: 1.36s; }
.hero__subtitle {
font-family: 'Inter', sans-serif;
font-size: clamp(1rem, 0.85rem + 0.8vw, 1.4rem);
font-variation-settings: 'wght' 300;
color: var(--vt-text-muted);
margin-top: 2.5rem;
max-width: 600px;
line-height: 1.6;
}
.hero__cta-group {
display: flex;
gap: 1rem;
margin-top: 3rem;
flex-wrap: wrap;
justify-content: center;
}
/* ========================================
Buttons
======================================== */
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.85rem 2rem;
border-radius: 8px;
border: 1px solid var(--vt-accent-blue);
background: var(--vt-accent-blue);
color: #ffffff;
font-family: 'Inter', sans-serif;
font-size: 0.875rem;
font-variation-settings: 'wght' 500;
letter-spacing: 0.01em;
cursor: pointer;
transition: all var(--vt-transition-base),
font-variation-settings var(--vt-transition-font);
}
.btn:hover {
font-variation-settings: 'wght' 700;
background: #1a4fd9;
box-shadow: var(--vt-shadow-glow-blue);
transform: translateY(-1px);
color: #ffffff;
}
.btn--ghost {
background: transparent;
color: var(--vt-accent-blue);
}
.btn--ghost:hover {
background: var(--vt-surface-hover);
box-shadow: none;
color: var(--vt-accent-blue);
}
/* ========================================
Section Utility
======================================== */
.section {
padding: 6rem 2rem;
scroll-snap-align: start;
}
.section--alt {
background: var(--vt-bg-charcoal);
}
.section__inner {
max-width: 1100px;
margin: 0 auto;
}
.section__eyebrow {
font-family: 'Recursive', monospace;
font-size: 0.75rem;
font-variation-settings: 'MONO' 1, 'CASL' 0, 'wght' 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--vt-accent-blue);
margin-bottom: 1rem;
}
.section__title {
font-family: 'Fraunces', Georgia, serif;
font-size: clamp(2rem, 1.5rem + 3vw, 4rem);
font-weight: 800;
font-variation-settings: 'opsz' 72, 'SOFT' 20, 'WONK' 1;
letter-spacing: -0.03em;
line-height: 1.1;
color: var(--vt-text-primary);
margin-bottom: 1.5rem;
}
.section__desc {
font-variation-settings: 'wght' 380;
font-size: 1.05rem;
color: var(--vt-text-muted);
max-width: 650px;
line-height: 1.7;
margin-bottom: 3rem;
}
/* ========================================
Card Grid
======================================== */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.card {
background: var(--vt-surface-glass);
border: 1px solid var(--vt-border-subtle);
border-radius: 12px;
padding: 2rem;
position: relative;
overflow: hidden;
transition: border-color var(--vt-transition-base),
box-shadow var(--vt-transition-base);
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 10%;
right: 10%;
height: 1px;
background: linear-gradient(
90deg,
transparent,
var(--vt-accent-blue),
var(--vt-accent-violet),
transparent
);
opacity: 0;
transition: opacity var(--vt-transition-base);
}
.card:hover {
border-color: rgba(41, 98, 255, 0.2);
box-shadow: var(--vt-shadow-glow-blue);
}
.card:hover::before {
opacity: 0.6;
}
.card__axis {
font-family: 'Recursive', monospace;
font-size: 0.7rem;
font-variation-settings: 'MONO' 1, 'CASL' 0, 'wght' 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--vt-accent-blue);
margin-bottom: 0.75rem;
}
.card__title {
font-family: 'Inter', sans-serif;
font-size: 1.2rem;
font-variation-settings: 'wght' 500;
letter-spacing: -0.02em;
color: var(--vt-text-primary);
margin-bottom: 0.75rem;
transition: font-variation-settings var(--vt-transition-font);
}
.card:hover .card__title {
font-variation-settings: 'wght' 800;
}
.card__text {
font-variation-settings: 'wght' 380;
font-size: 0.9rem;
line-height: 1.65;
color: var(--vt-text-muted);
}
/* ========================================
Interactive Axis Demo
======================================== */
.axis-demo {
background: var(--vt-bg-graphite);
border-radius: 16px;
padding: 3rem;
text-align: center;
}
.axis-demo__preview {
font-family: 'Roboto Flex', sans-serif;
font-size: clamp(2.5rem, 2rem + 4vw, 6rem);
line-height: 1.1;
letter-spacing: -0.03em;
color: var(--vt-text-primary);
margin-bottom: 2.5rem;
min-height: 1.3em;
}
.axis-demo__controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.5rem;
text-align: left;
}
.axis-control {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.axis-control__header {
display: flex;
justify-content: space-between;
align-items: center;
}
.axis-control__name {
font-family: 'Recursive', monospace;
font-size: 0.75rem;
font-variation-settings: 'MONO' 1, 'CASL' 0, 'wght' 500;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--vt-text-muted);
}
.axis-control__value {
font-family: 'Recursive', monospace;
font-size: 0.75rem;
font-variation-settings: 'MONO' 1, 'CASL' 0, 'wght' 400;
color: var(--vt-accent-blue);
}
.axis-slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
background: var(--vt-bg-dark);
border-radius: 2px;
outline: none;
cursor: pointer;
}
.axis-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
background: var(--vt-accent-blue);
border-radius: 50%;
border: 2px solid var(--vt-bg-graphite);
box-shadow: 0 0 8px rgba(41, 98, 255, 0.3);
}
.axis-slider::-moz-range-thumb {
width: 16px;
height: 16px;
background: var(--vt-accent-blue);
border-radius: 50%;
border: 2px solid var(--vt-bg-graphite);
box-shadow: 0 0 8px rgba(41, 98, 255, 0.3);
}
/* ========================================
Wave Text Demo
======================================== */
.wave-demo {
text-align: center;
padding: 4rem 0;
}
.wave-text {
font-family: 'Roboto Flex', sans-serif;
font-size: clamp(2.5rem, 1.5rem + 5vw, 6rem);
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--vt-text-primary);
}
.wave-text span {
display: inline-block;
animation: weight-wave 3s ease-in-out infinite;
}
@keyframes weight-wave {
0%, 100% { font-variation-settings: 'wght' 100; color: var(--vt-text-muted); }
50% { font-variation-settings: 'wght' 900; color: var(--vt-text-primary); }
}
.wave-text span:nth-child(1) { animation-delay: calc(0 * 0.1s); }
.wave-text span:nth-child(2) { animation-delay: calc(1 * 0.1s); }
.wave-text span:nth-child(3) { animation-delay: calc(2 * 0.1s); }
.wave-text span:nth-child(4) { animation-delay: calc(3 * 0.1s); }
.wave-text span:nth-child(5) { animation-delay: calc(4 * 0.1s); }
.wave-text span:nth-child(6) { animation-delay: calc(5 * 0.1s); }
.wave-text span:nth-child(7) { animation-delay: calc(6 * 0.1s); }
.wave-text span:nth-child(8) { animation-delay: calc(7 * 0.1s); }
.wave-text span:nth-child(9) { animation-delay: calc(8 * 0.1s); }
.wave-text span:nth-child(10) { animation-delay: calc(9 * 0.1s); }
.wave-text span:nth-child(11) { animation-delay: calc(10 * 0.1s); }
.wave-text span:nth-child(12) { animation-delay: calc(11 * 0.1s); }
.wave-text span:nth-child(13) { animation-delay: calc(12 * 0.1s); }
/* ========================================
Scroll-Weight Section
======================================== */
.scroll-weight {
min-height: 150vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.scroll-weight__text {
font-family: 'Roboto Flex', sans-serif;
font-size: clamp(3rem, 2rem + 6vw, 9rem);
line-height: 1.0;
letter-spacing: -0.04em;
color: var(--vt-text-primary);
position: sticky;
top: 50vh;
transform: translateY(-50%);
text-align: center;
transition: font-variation-settings 60ms linear;
}
/* ========================================
Casual-Formal Demo
======================================== */
.casual-demo {
text-align: center;
padding: 4rem 0;
}
.casual-demo__text {
font-family: 'Recursive', sans-serif;
font-size: clamp(2rem, 1.5rem + 3vw, 4.5rem);
line-height: 1.2;
color: var(--vt-text-primary);
font-variation-settings: 'CASL' 0, 'wght' 400, 'slnt' 0;
transition: font-variation-settings 800ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
cursor: pointer;
user-select: none;
}
.casual-demo__text:hover {
font-variation-settings: 'CASL' 1, 'wght' 800, 'slnt' -12;
color: var(--vt-accent-blue);
}
.casual-demo__label {
font-family: 'Recursive', monospace;
font-size: 0.75rem;
font-variation-settings: 'MONO' 1, 'CASL' 0, 'wght' 400;
color: var(--vt-text-muted);
margin-top: 1rem;
letter-spacing: 0.05em;
}
/* ========================================
Footer
======================================== */
.footer {
padding: 3rem 2rem;
text-align: center;
border-top: 1px solid var(--vt-border-subtle);
}
.footer__text {
font-family: 'Inter', sans-serif;
font-size: 0.85rem;
font-variation-settings: 'wght' 350;
color: var(--vt-text-muted);
}
.footer__credit {
font-family: 'Roboto Flex', sans-serif;
font-size: clamp(1.5rem, 1rem + 2vw, 3rem);
font-variation-settings: 'wght' 200, 'wdth' 100;
letter-spacing: -0.03em;
color: var(--vt-text-secondary);
margin-top: 1.5rem;
transition: font-variation-settings 600ms ease;
}
.footer__credit:hover {
font-variation-settings: 'wght' 900, 'wdth' 125;
}
/* ========================================
Reduced Motion
======================================== */
@media (prefers-reduced-motion: reduce) {
.hero__title span,
.wave-text span {
animation: none !important;
font-variation-settings: 'wght' 700, 'wdth' 100 !important;
}
* {
transition-duration: 0.01ms !important;
}
}
/* ========================================
Responsive
======================================== */
@media (max-width: 768px) {
.nav {
padding: 0.75rem 1.25rem;
}
.nav__links {
gap: 1.25rem;
}
.nav__link {
font-size: 0.8rem;
}
.section {
padding: 4rem 1.25rem;
}
.axis-demo {
padding: 2rem 1.5rem;
}
.card-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<!-- Navigation -->
<nav class="nav">
<a href="#" class="nav__logo">VarType</a>
<ul class="nav__links">
<li><a href="#axes" class="nav__link">Axes</a></li>
<li><a href="#wave" class="nav__link">Wave</a></li>
<li><a href="#interactive" class="nav__link">Interactive</a></li>
<li><a href="#scroll" class="nav__link">Scroll</a></li>
<li><a href="#casual" class="nav__link">Casual</a></li>
</ul>
</nav>
<!-- Hero -->
<section class="hero">
<p class="hero__eyebrow">Variable Font Aesthetic</p>
<h1 class="hero__title" aria-label="Variable Typography">
<span>V</span><span>a</span><span>r</span><span>i</span><span>a</span><span>b</span><span>l</span><span>e</span>
<br>
<span>T</span><span>y</span><span>p</span><span>o</span><span>g</span><span>r</span><span>a</span><span>p</span><span>h</span><span>y</span>
</h1>
<p class="hero__subtitle">
Dynamic responsive type that breathes, flows, and responds.
One font file. Infinite expressions. Every axis a design decision.
</p>
<div class="hero__cta-group">
<a href="#interactive" class="btn">Explore Axes</a>
<a href="#wave" class="btn btn--ghost">See the Wave</a>
</div>
</section>
<!-- Axis Feature Cards -->
<section id="axes" class="section section--alt">
<div class="section__inner">
<p class="section__eyebrow">Registered Axes</p>
<h2 class="section__title">The Five Dimensions of Type</h2>
<p class="section__desc">
Variable fonts expose continuous design axes that replace
discrete font files. Each axis is a slider from minimum to
maximum, interpolating every value in between.
</p>
<div class="card-grid">
<div class="card">
<p class="card__axis">wght 100-1000</p>
<h3 class="card__title">Weight</h3>
<p class="card__text">
Controls stroke thickness from hairline Thin to ultra-heavy
Black. The most commonly used axis, replacing the need for
separate Light, Regular, Medium, Bold, and Black font files.
</p>
</div>
<div class="card">
<p class="card__axis">wdth 25-200</p>
<h3 class="card__title">Width</h3>
<p class="card__text">
Adjusts the horizontal proportion of letterforms from
ultra-condensed to ultra-expanded. Essential for responsive
typography, allowing headlines to reflow naturally across
viewport sizes.
</p>
</div>
<div class="card">
<p class="card__axis">opsz 8-144</p>
<h3 class="card__title">Optical Size</h3>
<p class="card__text">
Adapts stroke contrast and detail to display size. Small
optical sizes use thicker strokes for legibility; large sizes
refine details for elegance. Browsers can apply this
automatically.
</p>
</div>
<div class="card">
<p class="card__axis">slnt -15-0</p>
<h3 class="card__title">Slant</h3>
<p class="card__text">
Tilts letterforms along a continuous oblique angle.
Unlike italic, slant does not change letterform construction,
making it ideal for subtle emphasis without changing the
typographic texture.
</p>
</div>
<div class="card">
<p class="card__axis">ital 0-1</p>
<h3 class="card__title">Italic</h3>
<p class="card__text">
A binary or continuous axis that activates true italic
letterform alternates. In variable fonts with both slant and
italic axes, this triggers actual italic letter shapes rather
than simple oblique slanting.
</p>
</div>
<div class="card">
<p class="card__axis">CASL, GRAD, SOFT...</p>
<h3 class="card__title">Custom Axes</h3>
<p class="card__text">
Font designers can define any custom axis: Casualness (CASL)
in Recursive, Grade (GRAD) in Roboto Flex, Softness (SOFT)
in Fraunces. These unlock unique personality dimensions
beyond standard typography.
</p>
</div>
</div>
</div>
</section>
<!-- Wave Text Demo -->
<section id="wave" class="section">
<div class="section__inner">
<p class="section__eyebrow">Kinetic Effect</p>
<h2 class="section__title">Weight Wave</h2>
<p class="section__desc">
Per-character staggered weight animation creates a ripple
effect across the text. Each letter cycles between thin and
black with a slight delay, producing an organic wave of
typographic emphasis.
</p>
<div class="wave-demo">
<p class="wave-text" aria-label="Fluid Motion">
<span>F</span><span>l</span><span>u</span><span>i</span><span>d</span><span> </span><span>M</span><span>o</span><span>t</span><span>i</span><span>o</span><span>n</span>
</p>
</div>
</div>
</section>
<!-- Interactive Axis Demo -->
<section id="interactive" class="section section--alt">
<div class="section__inner">
<p class="section__eyebrow">Interactive</p>
<h2 class="section__title">Explore the Design Space</h2>
<p class="section__desc">
Drag the sliders to manipulate variable font axes in real
time. The preview text responds instantly, letting you
feel the continuous interpolation between style extremes.
</p>
<div class="axis-demo">
<p class="axis-demo__preview" id="axisPreview">
Parametric Type
</p>
<div class="axis-demo__controls">
<div class="axis-control">
<div class="axis-control__header">
<span class="axis-control__name">Weight (wght)</span>
<span class="axis-control__value" id="wghtValue">400</span>
</div>
<input type="range" class="axis-slider" id="wghtSlider"
min="100" max="1000" value="400" step="1">
</div>
<div class="axis-control">
<div class="axis-control__header">
<span class="axis-control__name">Width (wdth)</span>
<span class="axis-control__value" id="wdthValue">100</span>
</div>
<input type="range" class="axis-slider" id="wdthSlider"
min="25" max="151" value="100" step="1">
</div>
<div class="axis-control">
<div class="axis-control__header">
<span class="axis-control__name">Optical Size (opsz)</span>
<span class="axis-control__value" id="opszValue">48</span>
</div>
<input type="range" class="axis-slider" id="opszSlider"
min="8" max="144" value="48" step="1">
</div>
</div>
</div>
</div>
</section>
<!-- Scroll-Linked Weight -->
<section id="scroll" class="scroll-weight">
<h2 class="scroll-weight__text" id="scrollText">
Scroll to transform
</h2>
</section>
<!-- Casual-to-Formal Demo -->
<section id="casual" class="section">
<div class="section__inner">
<p class="section__eyebrow">Custom Axis: CASL</p>
<h2 class="section__title">Casual to Formal</h2>
<p class="section__desc">
Recursive's Casualness axis transitions letterforms from
rational, linear geometry to friendly, handwritten curves.
Hover over the text below to see the shift.
</p>
<div class="casual-demo">
<p class="casual-demo__text">
Hover to shift personality
</p>
<p class="casual-demo__label">
Linear (CASL 0) → Casual (CASL 1)
</p>
</div>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<p class="footer__text">
Built with variable fonts from Google Fonts.
All axes animated with pure CSS and minimal JavaScript.
</p>
<p class="footer__credit">Variable Typography</p>
</footer>
<!-- JavaScript: Interactive Axis Sliders -->
<script>
(function() {
const preview = document.getElementById('axisPreview');
const sliders = {
wght: document.getElementById('wghtSlider'),
wdth: document.getElementById('wdthSlider'),
opsz: document.getElementById('opszSlider'),
};
const values = {
wght: document.getElementById('wghtValue'),
wdth: document.getElementById('wdthValue'),
opsz: document.getElementById('opszValue'),
};
function updatePreview() {
const wght = sliders.wght.value;
const wdth = sliders.wdth.value;
const opsz = sliders.opsz.value;
preview.style.fontVariationSettings =
`'wght' ${wght}, 'wdth' ${wdth}, 'opsz' ${opsz}`;
values.wght.textContent = wght;
values.wdth.textContent = wdth;
values.opsz.textContent = opsz;
}
Object.values(sliders).forEach(slider => {
slider.addEventListener('input', updatePreview);
});
updatePreview();
})();
// Scroll-linked weight
(function() {
const scrollText = document.getElementById('scrollText');
const section = scrollText.closest('.scroll-weight');
function onScroll() {
const rect = section.getBoundingClientRect();
const sectionHeight = section.offsetHeight;
const viewportH = window.innerHeight;
const scrolled = Math.max(0, -rect.top);
const total = sectionHeight - viewportH;
const progress = Math.min(1, Math.max(0, scrolled / total));
const wght = Math.round(100 + progress * 900);
const wdth = Math.round(75 + progress * 76);
scrollText.style.fontVariationSettings =
`'wght' ${wght}, 'wdth' ${wdth}`;
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
})();
</script>
</body>
</html>
pyftsubset (from fonttools) or Google Fonts’ built-in subsetting via the &text= URL parameter to strip unused glyphs and axes, reducing file size by 50-80%font-display: swap or optional: Variable font files can be larger than static fonts; ensure text remains visible during load by setting font-display in your @font-face declarations or Google Fonts importfont-variation-settings for registered axes: Use font-weight: 600 instead of font-variation-settings: 'wght' 600 for weight, width, slant, and optical size – this ensures proper cascade behavior and accessibility tool compatibilityfont-variation-settings only for custom axes and animation: Reserve the low-level property for designer-defined axes (CASL, GRAD, SOFT) and for CSS keyframe animations where you need to interpolate multiple axes simultaneouslypassive: true event listeners: Scroll-driven font axis changes must use passive scroll handlers to avoid jank; consider the new CSS animation-timeline: scroll() spec for pure-CSS scroll linking where browser support allowsprefers-reduced-motion from the start: Wrap all @keyframes animations and long transitions in a @media (prefers-reduced-motion: no-preference) query, or disable them with a reduce query as shown in the templatefont-variation-settings forces glyph re-rasterization on every frame; too many concurrent animations will drop frames on mobile devices and low-powered hardwareCache-Control: public, max-age=31536000, immutable headers for self-hosted variable font files; their single-file nature means one cached download covers all styles permanently