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.