Scroll-Driven Animations
Scroll-Driven Animations tie CSS animation progress to scroll position instead of time. This means an animation plays forward as you scroll down, backward as you scroll up, or based on when an element enters the viewport — all without JavaScript event listeners or IntersectionObserver.
The feature introduces two new timeline types: scroll() for progress tied to a scroll container, and view()for progress tied to an element's visibility in a scrollport. Both work with the standard CSS animation properties you already know.
| 1 | /* Scroll progress indicator */ |
| 2 | .scroll-progress { |
| 3 | position: fixed; |
| 4 | top: 0; |
| 5 | left: 0; |
| 6 | height: 3px; |
| 7 | background: #3B82F6; |
| 8 | transform-origin: left; |
| 9 | animation: grow-width linear; |
| 10 | animation-timeline: scroll(root); |
| 11 | } |
| 12 | |
| 13 | @keyframes grow-width { |
| 14 | from { transform: scaleX(0); } |
| 15 | to { transform: scaleX(1); } |
| 16 | } |
The scroll() function creates a timeline based on the scroll position of a scroll container. You can reference the root document scroll, a specific element, or a particular axis.
| 1 | /* Root scroll — entire page */ |
| 2 | animation-timeline: scroll(); |
| 3 | |
| 4 | /* Named scroll container */ |
| 5 | .scroller { |
| 6 | overflow-y: auto; |
| 7 | animation-timeline: scroll(nearest); |
| 8 | } |
| 9 | |
| 10 | /* Axis specification */ |
| 11 | animation-timeline: scroll(root block); /* vertical scroll */ |
| 12 | animation-timeline: scroll(root inline); /* horizontal scroll */ |
| 13 | |
| 14 | /* Full syntax */ |
| 15 | animation-timeline: scroll(<scroller> <axis>); |
| 16 | |
| 17 | /* Examples */ |
| 18 | .progress-bar { |
| 19 | animation-timeline: scroll(root block); |
| 20 | } |
| 21 | |
| 22 | .horizontal-progress { |
| 23 | animation-timeline: scroll(.carousel inline); |
| 24 | } |
| 25 | |
| 26 | /* Scroll snap container */ |
| 27 | .snap-container { |
| 28 | overflow-x: scroll; |
| 29 | scroll-snap-type: x mandatory; |
| 30 | animation-timeline: scroll(nearest inline); |
| 31 | } |
The scroll() function accepts three parameters: the scroller element (root, nearest, or self), and the axis (block, inline, or x/y). Default is the nearest scroll ancestor on the block axis.
note
The view()function creates a timeline based on an element's visibility within its scrollport. As the element scrolls into view, the animation progresses. As it scrolls out, the animation reverses. This is the basis for scroll-triggered reveal animations.
| 1 | /* Reveal animation — element fades in as it enters view */ |
| 2 | .reveal { |
| 3 | opacity: 0; |
| 4 | transform: translateY(40px); |
| 5 | animation: fade-up linear both; |
| 6 | animation-timeline: view(); |
| 7 | animation-range: entry 0% entry 100%; |
| 8 | } |
| 9 | |
| 10 | @keyframes fade-up { |
| 11 | from { |
| 12 | opacity: 0; |
| 13 | transform: translateY(40px); |
| 14 | } |
| 15 | to { |
| 16 | opacity: 1; |
| 17 | transform: translateY(0); |
| 18 | } |
| 19 | } |
| 20 | |
| 21 | /* Full syntax */ |
| 22 | animation-timeline: view(<axis> <range>); |
| 23 | |
| 24 | /* Specify the element to track (default: self) */ |
| 25 | animation-timeline: view(block); |
| 26 | |
| 27 | /* Track a different element */ |
| 28 | animation-timeline: view(of .scroll-container block); |
The animation-range property controls which part of the view progress triggers the animation. Common values include entry, exit, contain, and cover.
| 1 | /* Animation range keywords */ |
| 2 | .reveal { |
| 3 | animation-timeline: view(); |
| 4 | |
| 5 | /* Animate during the entry phase (0% to 100% of entry) */ |
| 6 | animation-range: entry 0% entry 100%; |
| 7 | |
| 8 | /* Animate during the entire visibility cycle */ |
| 9 | animation-range: cover 0% cover 100%; |
| 10 | |
| 11 | /* Animate while element is contained in viewport */ |
| 12 | animation-range: contain 0% contain 100%; |
| 13 | |
| 14 | /* Animate during exit */ |
| 15 | animation-range: exit 0% exit 100%; |
| 16 | |
| 17 | /* Combine entry and exit with custom percentages */ |
| 18 | animation-range: entry 0% exit 50%; |
| 19 | } |
The animation-timeline property replaces the time-based default with a scroll-based timeline. It accepts scroll(), view(), or a named timeline created with timeline-name.
| 1 | /* Named timeline — reusable across elements */ |
| 2 | .scroller { |
| 3 | overflow-y: auto; |
| 4 | scroll-timeline-name: --page-scroll; |
| 5 | scroll-timeline-axis: block; |
| 6 | } |
| 7 | |
| 8 | .progress-bar { |
| 9 | animation: grow linear; |
| 10 | animation-timeline: --page-scroll; |
| 11 | } |
| 12 | |
| 13 | /* Element-based named timeline */ |
| 14 | .card { |
| 15 | view-timeline-name: --card-view; |
| 16 | view-timeline-axis: block; |
| 17 | } |
| 18 | |
| 19 | .card-content { |
| 20 | animation: fade-in linear both; |
| 21 | animation-timeline: --card-view; |
| 22 | animation-range: entry 20% entry 80%; |
| 23 | } |
| 24 | |
| 25 | /* Multiple animations with different timelines */ |
| 26 | .element { |
| 27 | animation: |
| 28 | color-shift linear scroll(root block), |
| 29 | fade-in linear view(); |
| 30 | animation-timeline: |
| 31 | scroll(root block), |
| 32 | view(); |
| 33 | } |
Parallax effects — where background elements move at a different speed than foreground content — are a natural fit for scroll-driven animations. You can achieve this with a single CSS rule and zero JavaScript.
| 1 | /* Background parallax — moves slower than scroll */ |
| 2 | .parallax-bg { |
| 3 | position: fixed; |
| 4 | inset: 0; |
| 5 | z-index: -1; |
| 6 | animation: parallax-shift linear; |
| 7 | animation-timeline: scroll(root block); |
| 8 | } |
| 9 | |
| 10 | @keyframes parallax-shift { |
| 11 | from { transform: translateY(0); } |
| 12 | to { transform: translateY(30%); } |
| 13 | } |
| 14 | |
| 15 | /* Hero section parallax — fades out on scroll */ |
| 16 | .hero { |
| 17 | animation: hero-fade linear both; |
| 18 | animation-timeline: scroll(root block); |
| 19 | animation-range: 0% 30%; |
| 20 | } |
| 21 | |
| 22 | @keyframes hero-fade { |
| 23 | from { |
| 24 | opacity: 1; |
| 25 | transform: translateY(0); |
| 26 | } |
| 27 | to { |
| 28 | opacity: 0; |
| 29 | transform: translateY(-50px); |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | /* Multi-speed parallax layers */ |
| 34 | .parallax-layer-1 { |
| 35 | animation: layer1 linear; |
| 36 | animation-timeline: scroll(root block); |
| 37 | } |
| 38 | |
| 39 | .parallax-layer-2 { |
| 40 | animation: layer2 linear; |
| 41 | animation-timeline: scroll(root block); |
| 42 | } |
| 43 | |
| 44 | @keyframes layer1 { |
| 45 | from { transform: translateY(0); } |
| 46 | to { transform: translateY(20%); } |
| 47 | } |
| 48 | |
| 49 | @keyframes layer2 { |
| 50 | from { transform: translateY(0); } |
| 51 | to { transform: translateY(40%); } |
| 52 | } |
info
Reveal animations make elements fade, slide, or animate in as they scroll into view. Using view() timelines, you can create these effects for every element on the page without any JavaScript scroll detection.
| 1 | /* Base reveal class — apply to any element */ |
| 2 | .reveal-on-scroll { |
| 3 | opacity: 0; |
| 4 | transform: translateY(30px); |
| 5 | animation: reveal-up linear both; |
| 6 | animation-timeline: view(); |
| 7 | animation-range: entry 10% entry 90%; |
| 8 | } |
| 9 | |
| 10 | @keyframes reveal-up { |
| 11 | from { |
| 12 | opacity: 0; |
| 13 | transform: translateY(30px); |
| 14 | } |
| 15 | to { |
| 16 | opacity: 1; |
| 17 | transform: translateY(0); |
| 18 | } |
| 19 | } |
| 20 | |
| 21 | /* Slide from left */ |
| 22 | .reveal-left { |
| 23 | opacity: 0; |
| 24 | transform: translateX(-40px); |
| 25 | animation: reveal-from-left linear both; |
| 26 | animation-timeline: view(); |
| 27 | animation-range: entry 10% entry 90%; |
| 28 | } |
| 29 | |
| 30 | @keyframes reveal-from-left { |
| 31 | from { |
| 32 | opacity: 0; |
| 33 | transform: translateX(-40px); |
| 34 | } |
| 35 | to { |
| 36 | opacity: 1; |
| 37 | transform: translateX(0); |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | /* Scale up reveal */ |
| 42 | .reveal-scale { |
| 43 | opacity: 0; |
| 44 | transform: scale(0.9); |
| 45 | animation: reveal-scale-up linear both; |
| 46 | animation-timeline: view(); |
| 47 | animation-range: entry 5% entry 85%; |
| 48 | } |
| 49 | |
| 50 | @keyframes reveal-scale-up { |
| 51 | from { |
| 52 | opacity: 0; |
| 53 | transform: scale(0.9); |
| 54 | } |
| 55 | to { |
| 56 | opacity: 1; |
| 57 | transform: scale(1); |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | /* Staggered children — use animation-delay with view() */ |
| 62 | .stagger-list > * { |
| 63 | animation: reveal-up linear both; |
| 64 | animation-timeline: view(); |
| 65 | animation-range: entry 10% entry 90%; |
| 66 | } |
| 67 | |
| 68 | .stagger-list > *:nth-child(1) { animation-delay: 0ms; } |
| 69 | .stagger-list > *:nth-child(2) { animation-delay: 50ms; } |
| 70 | .stagger-list > *:nth-child(3) { animation-delay: 100ms; } |
| 71 | .stagger-list > *:nth-child(4) { animation-delay: 150ms; } |
| 72 | .stagger-list > *:nth-child(5) { animation-delay: 200ms; } |
Scroll-driven animations are designed for performance, but there are important guidelines to follow to keep your animations smooth at 60fps.
| 1 | /* ✓ Good — GPU-composited */ |
| 2 | .reveal { |
| 3 | animation: fade-up linear both; |
| 4 | animation-timeline: view(); |
| 5 | animation-range: entry 10% entry 90%; |
| 6 | will-change: transform, opacity; |
| 7 | } |
| 8 | |
| 9 | @keyframes fade-up { |
| 10 | from { opacity: 0; transform: translateY(30px); } |
| 11 | to { opacity: 1; transform: translateY(0); } |
| 12 | } |
| 13 | |
| 14 | /* ✗ Bad — triggers layout on every frame */ |
| 15 | .bad-reveal { |
| 16 | animation: slide-bad linear both; |
| 17 | animation-timeline: view(); |
| 18 | } |
| 19 | |
| 20 | @keyframes slide-bad { |
| 21 | from { margin-top: 100px; opacity: 0; } |
| 22 | to { margin-top: 0; opacity: 1; } |
| 23 | } |
warning
Scroll-Driven Animations have growing browser support. Chrome and Edge support the feature; Firefox and Safari are working on implementation.
| Browser | Support | Version |
|---|---|---|
| Chrome | ✓ | 115+ |
| Edge | ✓ | 115+ |
| Firefox | ◐ | In development |
| Safari | ◐ | In development |