Articles /

animationperformance

Functional motion & scroll-driven animations

Using scroll and view timelines for UX, not just decoration.

The shift from decorative to functional motion

Motion on the web has a reputation problem: it's associated with unnecessary flair — things sliding in for no reason, carousels auto-playing, parallax that makes people motion-sick. "Functional motion" is the counter-principle: animation that communicates something, confirms an action, or guides the user through content. In 2026, CSS gives us native tools to build this without JavaScript.

The two core tools are scroll timelines (animation progress tied to how much you've scrolled) and view timelines (animation progress tied to when an element enters or exits the viewport).

Scroll timelines

A scroll timeline links an animation's progress to scroll position. The animation plays forward as you scroll down and backward as you scroll up.

/* Reading progress bar */
.progress-bar {
  width: 0%;
  animation: fill linear;
  animation-timeline: scroll(); /* document scroller */
}
@keyframes fill { to { width: 100%; } }

/* Scope to a specific container */
.scroll-container {
  overflow-y: scroll;
}
.inner-bar {
  animation: fill linear;
  animation-timeline: scroll(self); /* 'self' = nearest scroll ancestor */
}

scroll() defaults to the nearest scroll container. Pass scroll(root) to always use the document, or scroll(self) to use the element's own scroll container. The animation is driven by the browser's compositor — it doesn't run on the main thread and doesn't block JavaScript.

View timelines

A view timeline links animation progress to when an element enters or exits the scrollport (the visible area of the scroll container). This is the native "animate in when visible" pattern.

/* Fade and slide in when element enters viewport */
.reveal {
  animation: fade-up linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 30%;
  /* Plays from 0% to 100% as the element enters the top 30% of the viewport */
}
@keyframes fade-up {
  from { opacity: 0; translate: 0 2rem; }
  to   { opacity: 1; translate: 0 0; }
}

/* Pin/parallax effect */
.hero-text {
  animation: parallax linear;
  animation-timeline: view();
  animation-range: exit 0% exit 100%;
}
@keyframes parallax {
  to { translate: 0 -4rem; }
}

animation-range lets you control when during the element's journey the animation plays: entry is when it enters the scroll container, exit is when it leaves, and you can use percentages to fine-tune the range.

Functional use cases

  • Reading indicator — scroll progress bar shows how far through an article the user is.
  • Reveal on scroll — sections fade in as they enter the viewport, reducing cognitive load by showing content progressively.
  • Sticky header shadow — a shadow appears on the header when content is scrolled behind it (combine with scroll-state container queries).
  • Table of contents highlight — the active section link highlights as the corresponding heading enters the viewport.
  • Parallax depth — background elements move at different rates, adding depth to a hero section.

Accessibility: prefers-reduced-motion

Always wrap scroll-driven animations in a prefers-reduced-motion: no-preference block or reset them in a reduce block. Users with vestibular disorders, motion sensitivity, or epilepsy may need motion disabled or reduced.

@media (prefers-reduced-motion: reduce) {
  .reveal {
    animation: none;
    opacity: 1;
    translate: 0 0;
  }
  .progress-bar {
    animation: none;
    width: 100%;
    opacity: 0.15; /* subtle static indicator */
  }
}
Functional motion should degrade gracefully — the content must be usable without the animation. Never hide content in an opacity: 0 animation without a fallback.

Browser support

Scroll-driven animations (both scroll() and view() timelines) are supported in Chrome 115+, Edge 115+, and Safari 18+. Firefox is implementing them. In 2026, they're widely usable with a reduce-motion fallback. Use @supports (animation-timeline: scroll()) to apply them progressively.