Scroll Timeline

Create engaging scroll-driven animations with pure CSS. Connect animation progress directly to scroll position for responsive, performant interfaces.

What Is Scroll Timeline?

Scroll-driven animations represent a paradigm shift in how we think about CSS animations. Traditional CSS animations progress based on time -- a two-second transition moves from start to finish regardless of what the user does. Scroll-driven animations, however, progress based on the user's scroll position, creating a direct connection between user behavior and visual feedback. According to the Design.dev CSS Scroll-Driven Animations Guide, this approach transforms how we create engaging user experiences.

The scroll-timeline CSS property defines a named scroll progress timeline that progresses through scrolling a scrollable element (scroller) between top and bottom or left and right. As documented by MDN Web Docs, this timeline becomes the reference point for animation progress, replacing the default time-based timeline that animations traditionally use.

Before scroll-driven animations, creating scroll-linked effects required JavaScript event listeners, which introduced performance overhead and complexity. Scroll timeline brings this capability natively to CSS, enabling performant scroll-driven effects with just a few lines of declarative code.

The key insight is that scroll position becomes the timeline. When a user scrolls down 500 pixels, that scroll distance directly maps to animation progress. This creates an intuitive relationship between input (scrolling) and output (animation), making interfaces feel more responsive and connected to user intent. For projects requiring advanced animation capabilities, our web development services can help implement these techniques effectively.

The key insight is that scroll position becomes the timeline. When a user scrolls down 500 pixels, that scroll distance directly maps to animation progress. This creates an intuitive relationship between input (scrolling) and output (animation), making interfaces feel more responsive and connected to user intent.

Three Components of Scroll-Driven Animations

Every scroll-driven animation consists of these essential components working together:

The Target

The element on the page that you're animating. This can be any element -- a hero section, a card, a navigation bar, or decorative graphics.

The Keyframes

What happens to the target element during the animation. These are traditional CSS @keyframes that define the start, middle, and end states.

The Timeline

What determines animation progress. Instead of time, scroll position drives the animation forward -- this is the revolutionary part.

Core Properties and Syntax

animation-timeline Property

The animation-timeline property connects an animation to a scroll timeline instead of the default time-based timeline. This property accepts several values that determine how the animation relates to scroll position. As documented in the Design.dev guide:

/* Connect animation to scroll position */
.element {
 animation: fadeIn linear;
 animation-timeline: scroll();
}

/* Connect to named timeline */
.element {
 animation: slideIn linear;
 animation-timeline: --my-scroller;
}

/* Use view timeline for viewport-based triggers */
.element {
 animation: reveal linear;
 animation-timeline: view();
}

/* Default time-based animation */
.element {
 animation: fadeIn 1s ease-out;
 animation-timeline: auto;
}

The animation-timeline property must be set after the animation shorthand for proper cascading. This ordering matters because CSS processes declarations in order, and the timeline needs to be established before the animation references it.

scroll-timeline-name and scroll-timeline-axis

These two properties work together to define named scroll timelines that can be shared across multiple elements. According to MDN Web Docs, the scroll-timeline-name property sets an identifier for the timeline, while scroll-timeline-axis specifies which scroll direction drives the animation:

/* Define a named timeline on a scroll container */
.scroll-container {
 scroll-timeline-name: --my-scroller;
 scroll-timeline-axis: block;
}

/* Use inline axis for horizontal scrolling */
.gallery-wrapper {
 scroll-timeline-name: --gallery-timeline;
 scroll-timeline-axis: inline;
}

The axis values include block (vertical, default), inline (horizontal), x (always horizontal), and y (always vertical). The block and inline values adapt to the document's writing mode, making them ideal for internationalized sites that support right-to-left or vertical text layouts.

Named timelines enable sophisticated scenarios where multiple elements respond to the same scroll position in different ways. For example, a navigation sidebar might highlight different sections while a progress bar fills simultaneously, both driven by the same named timeline.

scroll-timeline Shorthand

The scroll-timeline shorthand combines scroll-timeline-name and scroll-timeline-axis into a single declaration, reducing code verbosity. Per MDN documentation:

/* Shorthand: name first, then axis */
.container {
 scroll-timeline: --section-scroll block;
}

/* Both values optional */
.container {
 scroll-timeline: none;
 scroll-timeline: --custom inline;
}

Named timeline values must be dashed identifiers (starting with --), which prevents conflicts with current and future CSS keywords. This naming convention ensures your timeline names won't clash with browser-implemented features.

scroll() Function

The scroll() function creates a timeline based on a scroll container's scroll position. This timeline progresses as the user scrolls through the specified container, making it ideal for effects that should play continuously during scrolling.

Syntax and Parameters

animation-timeline: scroll(<scroller> <axis>);

/* Scroller values */
scroll() /* nearest scrollable ancestor */
scroll(root) /* document root (default) */
scroll(nearest) /* nearest ancestor with overflow scroll */
scroll(self) /* element itself */

/* Axis values */
scroll(block) /* vertical scroll (default) */
scroll(inline) /* horizontal scroll */
scroll(y) /* always vertical */
scroll(x) /* always horizontal */

Reading Progress Bar Example

One of the most common uses of scroll() is creating progress indicators that show how far a user has read through a page or article. As demonstrated in the WebKit Scroll-Driven Animations guide:

.reading-progress {
 position: fixed;
 top: 0;
 left: 0;
 height: 4px;
 background: linear-gradient(to right, #667eea, #764ba2);
 transform-origin: left;
 animation: grow-progress linear;
 animation-timeline: scroll(root block);
 z-index: 1000;
}

@keyframes grow-progress {
 from {
 transform: scaleX(0);
 }
 to {
 transform: scaleX(1);
 }
}

This creates a fixed bar at the top of the viewport that fills from left to right as the user scrolls through the document. The animation tracks exactly with scroll position, creating a precise representation of reading progress.

Element Rotation with Sticky Positioning

Another engaging effect is rotating or transforming elements as the user scrolls, creating a sense of momentum or progression. As illustrated by WebKit:

.scroll-indicator {
 position: sticky;
 top: 50%;
 transform: translateY(-50%);
 animation: rotate-on-scroll linear;
 animation-timeline: scroll(nearest block);
}

@keyframes rotate-on-scroll {
 from {
 transform: translateY(-50%) rotate(0deg);
 }
 to {
 transform: translateY(-50%) rotate(720deg);
 }
}

When combined with sticky positioning, scroll() enables creative effects where elements animate while remaining visible in the viewport as surrounding content scrolls past. The element stays pinned at 50% of the viewport height while continuously rotating based on scroll distance.

When to Use scroll()

Use scroll() when you want animations to play continuously during scrolling, without regard for what content is visible. Progress indicators, elements that rotate or transform based on scroll distance, and effects that should span the entire scroll experience are ideal candidates for scroll().

The key characteristic of scroll() is that it doesn't care about viewport visibility. If you scroll past the element entirely, the animation continues playing if you're still scrolling the container.

For frontend optimization strategies, understanding when to use scroll() versus view() is essential for creating performant animations that enhance rather than hinder user experience.

view() Function

The view() function creates a timeline based on when an element enters and exits the viewport. This is perfect for scroll-triggered reveal animations where you want elements to animate when they become visible to the user. According to the Design.dev guide, this function is essential for creating modern reveal effects.

How View Timeline Works

The view() timeline progresses from 0% to 100% as the target element moves through the viewport:

  • 0%: Element's bottom edge enters the viewport
  • 50%: Element is centered in the viewport
  • 100%: Element's top edge exits the viewport

Fade-In on Scroll Example

A classic reveal effect where elements fade and slide into place as they enter the viewport. As documented by WebKit:

.fade-in-on-scroll {
 opacity: 0;
 animation: fadeInUp linear forwards;
 animation-timeline: view();
 animation-range: entry 0% entry 100%;
}

@keyframes fadeInUp {
 from {
 opacity: 0;
 transform: translateY(50px);
 }
 to {
 opacity: 1;
 transform: translateY(0);
 }
}

This effect creates a smooth entrance animation that begins when the element's bottom edge enters the viewport and completes when the element is fully visible. The animation-range property controls exactly when the animation plays.

Scale and Rotate on Entry

More dramatic reveals that add visual interest to content sections. Per Design.dev's examples:

.scale-in-element {
 animation: scaleIn ease-out forwards;
 animation-timeline: view();
 animation-range: entry 0% cover 50%;
}

@keyframes scaleIn {
 from {
 transform: scale(0.5) rotate(-5deg);
 opacity: 0;
 }
 to {
 transform: scale(1) rotate(0deg);
 opacity: 1;
 }
}

The animation-range property controls how much of the view timeline triggers the animation. Here, the animation runs from entry start through 50% of coverage, creating a more pronounced effect that transforms and rotates the element into view.

View Timeline with Inset

The inset parameter adjusts the effective viewport boundaries for the timeline:

/* Start animation 100px before element enters viewport */
.early-start {
 animation: fadeIn linear;
 animation-timeline: view(-100px);
}

/* Different inset for top/bottom and left/right */
.custom-bounds {
 animation: slideIn linear;
 animation-timeline: view(10% 20%);
}

When to Use view()

Use view() when you want animations that respond to element visibility in the viewport. Reveal effects, section transitions, card entrance animations, and any animation that should happen when content becomes visible are perfect for view().

The key characteristic of view() is its focus on the target element's position relative to the viewport. The animation progresses based on the element's journey through the viewport, regardless of total scroll distance.

For creating visually stunning interfaces that feel responsive and modern, consider how these techniques integrate with our UI/UX design services to deliver exceptional user experiences.

animation-range Property

The animation-range property controls which portion of the scroll timeline triggers the animation. By default, animations run for the entire scroll range (0-100%), but you can customize this to create more refined effects. As detailed in the Design.dev guide.

Range Keywords

For view() timelines, several named keywords describe common animation phases:

animation-range: cover; /* Entire time element is in viewport */
animation-range: contain; /* When fully contained in viewport */
animation-range: entry; /* While entering viewport */
animation-range: exit; /* While exiting viewport */

Combining Keywords with Percentages

/* Animation plays only while entering */
animation-range: entry 0% entry 100%;

/* Animation plays during the middle portion */
animation-range: entry 50% exit 50%;

/* First half of the element's time in viewport */
animation-range: cover 0% cover 50%;

/* Combine with scroll distance (pixels or percentages) */
animation-range: 0px 500px;
animation-range: 25% 75%;

Reveal and Stay Pattern

Common pattern where elements fade in when entering but remain visible without continuing animation. Per WebKit's implementation guide:

.reveal-and-stay {
 opacity: 0;
 animation: fadeIn linear forwards;
 animation-timeline: view();
 animation-range: entry 0% entry 100%;
}

@keyframes fadeIn {
 from { opacity: 0; }
 to { opacity: 1; }
}

The key is animation-fill-mode: forwards (shorthand: linear forwards), which maintains the final state after the animation completes. Combined with a limited animation-range, this creates a reveal effect that doesn't replay as the element continues through the viewport.

Parallax with animation-range

Parallax effects that play only while the element is visible. As shown in the Design.dev examples:

.parallax-element {
 animation: parallaxMove linear;
 animation-timeline: view();
 animation-range: cover 0% cover 100%;
}

@keyframes parallaxMove {
 from {
 transform: translateY(100px);
 }
 to {
 transform: translateY(-100px);
 }
}

This creates a subtle parallax effect that plays throughout the element's time in the viewport, creating depth without overwhelming the user.

Separate Properties

Animation-range has two constituent properties for explicit control:

.element {
 animation-range-start: entry 0%;
 animation-range-end: exit 100%;
}

/* Shorthand */
.element {
 animation-range: entry 0% exit 100%;
}

Understanding how to combine these properties is key to creating sophisticated scroll-driven animations. For teams looking to implement these techniques at scale, our web development services can help establish animation guidelines and best practices.

Best Practices for Scroll-Driven Animations

Performance Optimization

Scroll-driven animations run on the compositor thread, but only if you animate the right properties. Animating layout properties like height, width, margin, or padding will force layout recalculations on every frame, causing janky scrolling. As emphasized in the Design.dev guide.

Always animate only transform and opacity:

/* GOOD - GPU-accelerated properties */
@keyframes performant-animation {
 from {
 opacity: 0;
 transform: translateY(20px);
 }
 to {
 opacity: 1;
 transform: translateY(0);
 }
}

/* BAD - Triggers layout recalculation */
@keyframes bad-animation {
 from { height: 0; margin-top: 50px; }
 to { height: 200px; margin-top: 0; }
}

The will-change property can hint to the browser which properties will animate, allowing it to optimize accordingly. Use it sparingly, as excessive optimization hints can consume memory.

.animated-element {
 will-change: transform, opacity;
 animation: slideIn linear;
 animation-timeline: view();
}

Accessibility Considerations

Motion-sensitive users may experience discomfort from scroll-driven animations, especially those that create a sense of movement through space. As recommended by WebKit, always consider reduced motion preferences:

/* Disable scroll animations for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
 .scroll-animated {
 animation: none;
 opacity: 1;
 transform: none;
 }
}

/* Or use positive opt-in */
@media not (prefers-reduced-motion) {
 .scroll-animated {
 animation: fadeIn linear;
 animation-timeline: view();
 }
}

Progressive Enhancement with @supports

Scroll-driven animations are a cutting-edge feature with growing but not universal browser support. Implement fallbacks to ensure your site functions for all users. As documented by Design.dev:

/* Base styles - work everywhere */
.element {
 opacity: 0;
 transform: translateY(20px);
 transition: all 0.6s ease-out;
}

/* Enhanced styles - only when scroll-timeline is supported */
@supports (animation-timeline: view()) {
 .element {
 opacity: 1;
 transform: none;
 transition: none;
 animation: fadeInUp linear;
 animation-timeline: view();
 animation-range: entry 0% entry 100%;
 }

 @keyframes fadeInUp {
 from {
 opacity: 0;
 transform: translateY(20px);
 }
 to {
 opacity: 1;
 transform: translateY(0);
 }
 }
}

JavaScript Feature Detection

// Check for scroll-timeline support
if (CSS.supports('animation-timeline', 'scroll()')) {
 document.body.classList.add('scroll-timeline-supported');
} else {
 console.log('Scroll-timeline not supported, using fallback');
}

Design Guidelines

  • Subtlety is Key: Scroll animations should enhance content, not compete with it
  • Consistency Matters: Use consistent animation timing across similar elements
  • Purpose-Driven Effects: Every scroll animation should serve a purpose
  • Test Across Devices: Scroll behavior varies across platforms and browsers

These best practices align with our approach to frontend development, where performance and accessibility are always priorities.

Browser Support and Compatibility

Current Support

Scroll-driven animations are implemented in Chromium-based browsers and are making progress in others:

BrowserSupportSince
ChromeFull supportVersion 115+ (July 2023)
EdgeFull supportVersion 115+ (July 2023)
OperaFull supportVersion 101+ (August 2023)
SafariAvailable in preview, expected in Safari 18+In development
FirefoxBehind feature flagNo stable support yet

Per WebKit's announcements and MDN's compatibility data.

Feature Detection

Always detect support rather than browser detection:

@supports (animation-timeline: scroll()) {
 .element {
 animation: fadeIn linear;
 animation-timeline: scroll();
 }
}

@supports (animation-timeline: view()) {
 .element {
 animation: fadeIn linear;
 animation-timeline: view();
 }
}

Polyfill Usage

For projects requiring broad browser support, the scroll-timeline polyfill provides a compatibility layer:

<script src="https://flackr.github.io/scroll-timeline/dist/scroll-timeline.js"></script>

The polyfill implements the scroll-driven animations specification using JavaScript, enabling the effects on browsers that don't support them natively. Test thoroughly with the polyfill, as performance characteristics differ from native implementations.

For projects that need broad browser compatibility, our web development team can help implement proper fallbacks and progressive enhancement strategies.

Practical Implementation Examples

Reading Progress Bar

A minimal implementation that shows how far a user has scrolled through an article. As implemented in the WebKit examples:

.progress-bar {
 position: fixed;
 top: 0;
 left: 0;
 right: 0;
 height: 4px;
 background: linear-gradient(to right, #667eea, #764ba2);
 transform-origin: left center;
 z-index: 1000;

 animation: reading-progress linear;
 animation-timeline: scroll(root block);
}

@keyframes reading-progress {
 from {
 transform: scaleX(0);
 }
 to {
 transform: scaleX(1);
 }
}

Parallax Hero Section

Create depth and visual interest with background elements that move at different speeds than foreground content.

.hero {
 position: relative;
 height: 100vh;
 overflow: hidden;
}

.hero-background {
 position: absolute;
 inset: 0;
 background: url('hero.jpg') center/cover;

 animation: parallax-bg linear;
 animation-timeline: view();
 animation-range: entry 0% exit 100%;
}

@keyframes parallax-bg {
 from {
 transform: translateY(0) scale(1.2);
 }
 to {
 transform: translateY(-100px) scale(1);
 }
}

.hero-content {
 animation: parallax-text linear;
 animation-timeline: view();
 animation-range: entry 0% exit 100%;
}

@keyframes parallax-text {
 from {
 transform: translateY(0);
 opacity: 1;
 }
 to {
 transform: translateY(-50px);
 opacity: 0;
 }
}

Staggered Card Grid

Reveal cards one after another as they enter the viewport, creating a cascade effect.

.card {
 opacity: 0;
 animation: card-fade-in ease-out forwards;
 animation-timeline: view();
 animation-range: entry 0% entry 50%;
}

/* Stagger based on child position */
.card:nth-child(1) { animation-delay: 0s; }
.card:nth-child(2) { animation-delay: 0.1s; }
.card:nth-child(3) { animation-delay: 0.2s; }
.card:nth-child(4) { animation-delay: 0.3s; }
.card:nth-child(5) { animation-delay: 0.4s; }

@keyframes card-fade-in {
 from {
 opacity: 0;
 transform: translateY(30px) scale(0.95);
 }
 to {
 opacity: 1;
 transform: translateY(0) scale(1);
 }
}

Horizontal Scroll Gallery

Create galleries where items scale and focus as they scroll into view horizontally.

.gallery-wrapper {
 overflow-x: auto;
 scroll-timeline-name: --gallery;
 scroll-timeline-axis: inline;
}

.gallery-item {
 animation: item-focus linear;
 animation-timeline: --gallery;
}

@keyframes item-focus {
 0% {
 transform: scale(0.8);
 filter: brightness(0.7);
 }
 50% {
 transform: scale(1);
 filter: brightness(1);
 }
 100% {
 transform: scale(0.8);
 filter: brightness(0.7);
 }
}

Section Navigation Highlighter

Highlight navigation items based on which section is currently in view.

/* Define named view timelines for each section */
section {
 view-timeline-name: var(--section-name);
 view-timeline-axis: block;
}

#introduction { --section-name: --intro; }
#features { --section-name: --features; }
#pricing { --section-name: --pricing; }
#contact { --section-name: --contact; }

/* Navigation items respond to their section */
.nav-link[href="#introduction"] {
 animation: navHighlight linear;
 animation-timeline: --intro;
 animation-range: entry 0% exit 100%;
}

.nav-link[href="#features"] {
 animation: navHighlight linear;
 animation-timeline: --features;
 animation-range: entry 0% exit 100%;
}

@keyframes navHighlight {
 0%, 100% {
 color: inherit;
 border-color: transparent;
 }
 10%, 90% {
 color: #667eea;
 border-color: #667eea;
 }
}

These practical examples demonstrate how scroll-driven animations can enhance user interfaces. For teams implementing these patterns, our UI/UX design services can provide guidance on animation strategy and implementation.

Frequently Asked Questions

Conclusion

Scroll timeline represents a significant advancement in CSS capabilities, enabling scroll-driven animations that previously required JavaScript libraries. By connecting animation progress directly to scroll position, these features create more responsive, engaging interfaces that feel connected to user behavior.

The key to effective scroll-driven animations lies in restraint and purpose. Use scroll() for continuous effects tied to scroll distance, view() for reveal animations based on element visibility, and animation-range to precisely control when animations play. Always consider performance by animating only transform and opacity, and respect accessibility by checking for reduced motion preferences.

As browser support continues to expand, scroll-driven animations will become an increasingly standard tool for creating polished, professional interfaces. Start experimenting now to understand how these features can enhance your projects while following best practices for progressive enhancement.

For more information on creating engaging user interfaces, explore our UI/UX design services or learn about frontend development best practices.

Ready to Create Engaging User Interfaces?

Our UI/UX design team specializes in creating responsive, animated interfaces that delight users and drive conversions.