Map Scroll Progress to Keyframe Animations
Drive CSS keyframe animations with an element's viewport position instead of duration by adding animation-timeline: view(). This scrubs through keyframes from 0% (element bottom enters viewport) to 100% (element top exits viewport). For example:
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
.elem {
animation: fadeIn;
animation-timeline: view();
}
Scrolling advances the animation proportionally to the element's viewport coverage, measured as a percentage (100% at full entry, 0% at full exit). Apply standard timing functions like cubic-bezier(0.15, 0.75, 0.35, 1) for ease-out effects or linear() for springs:
.box {
animation: spin var(--spring);
animation-timeline: view();
}
This leverages existing keyframe knowledge—no new syntax for basics—while avoiding JavaScript for simple scroll effects.
Control Animation Timing with Ranges
Override default cover range (full viewport traversal) using animation-range to start/end at specific points. contain triggers only when fully in viewport, ideal for complete animations like offscreen slides:
.shape {
animation: slideIn backwards;
animation-timeline: view();
animation-range: contain;
}
entry animates during top-to-bottom entry (perfect for fade-ins on images), exit during top-edge exit (fade-outs). Combine via comma-separated values:
img {
animation: fadeIn linear, fadeOut linear;
animation-timeline: view(), view();
animation-range: entry, exit;
}
For precision, use percentages: animation-range: cover 0% cover 50% starts at first pixel entry, ends at viewport midpoint. Long-form animation-range-start: cover 0%; animation-range-end: cover 50%; offers clarity. Mix ranges like contain 0% to exit 50% for extended effects.
Use scroll() timeline for global progress, like fixed reading indicators:
.readingIndicator {
animation: expand linear;
animation-timeline: scroll();
}
This scales a bar from 0 to total page scroll distance, though scrollbars often suffice.
Link Timelines Between Elements
Decouple tracking from animation: name a view-timeline on the trigger element, reference it elsewhere via timeline-scope on a shared ancestor.
main {
timeline-scope: --tracked-elem;
}
.content {
view-timeline: --tracked-elem;
}
.square {
animation: fadeIn backwards, fadeOut forwards;
animation-timeline: --tracked-elem, --tracked-elem;
animation-range: entry, exit;
}
Scroll on .content fades sticky .square, even if not descendants—timeline-scope propagates the named timeline. Limit: names are scoped to creator and descendants unless elevated.