Lab 4 min read

Variable Font Scroll

Variable font axes are animatable CSS properties, which means animation-timeline can drive font weight, letter-spacing, and more directly from scroll position.

CSSAnimationScrollVariable FontsNo JavaScript

font-variation-settings is an animatable CSS property. That means @keyframes can target it — and once a property is in a keyframe, animation-timeline can drive it from scroll position. No JavaScript, no font loading tricks, no canvas.

The pattern: a tall outer wrapper creates scroll space, a sticky inner element pins to the viewport, and a view-timeline-name on the outer wrapper drives everything inside. The page scroll is the only scroll that matters.

Scroll progress

As you scroll through the section, weight and tracking animate together — heavier type naturally needs tighter tracking.

page scroll → font-variation-settings
wght

Weight.

Follows.

Scroll.

scroll ↓

.outer {
  height: 300vh;
  view-timeline-name: --section;
  view-timeline-axis: block;
}

.sticky {
  position: sticky;
  top: 0;
}

.heading {
  font-variation-settings: 'wght' 300;
  animation: weight linear both;
  animation-timeline: --section;
  animation-range: contain 0% contain 100%;
}

@keyframes weight {
  from {
    font-variation-settings: 'wght' 300;
    letter-spacing:  0.06em;
  }
  to {
    font-variation-settings: 'wght' 700;
    letter-spacing: -0.03em;
  }
}

animation-range: contain 0% contain 100% runs the animation from the moment the outer section covers the full viewport to the moment it stops — locking the sticky element in place for exactly that duration.

Weight lens

Each word peaks at bold as you scroll through its slice of the range. The keyframe pulses light → heavy → light, and animation-range staggers the peak per item. The result: weight follows wherever you’re looking.

view-timeline weight lens — page scroll
variable
font
weight
axis
scroll
driven
timeline
motion
.outer {
  height: 500vh;
  view-timeline-name: --section;
}

.item {
  animation: lens linear both;
  animation-timeline: --section;
}

/* Stagger the peak per item */
.item:nth-child(1) { animation-range: contain  0% contain 16%; }
.item:nth-child(2) { animation-range: contain 13% contain 29%; }
/* … */

@keyframes lens {
  0%, 100% { font-variation-settings: 'wght' 300; }
  50%       { font-variation-settings: 'wght' 700; }
}

The 3% overlap between adjacent items means two words are partially animated at the same time during the transition, which feels smoother than a hard cutoff.

Browser support

Both demos use animation-timeline. That requires Safari 18 (iOS 18, September 2024). On older iOS the text renders at the light weight start state and stays there. font-variation-settings itself has been in Safari since version 11, so the type still loads and displays correctly.