Animate elements on scroll with Scroll-driven animations Stay organized with collections Save and categorize content based on your preferences.
Learn how to work with Scroll Timelines and View Timelines to create scroll-driven animations in a declarative way.
Published: May 5, 2023
Tip: Not a fan of reading? Gocheck out “Unleash the Power of Scroll-Driven Animations”, a 10-part video course that teaches you all there is to know about scroll-driven animations.Scroll-driven animations
Scroll-driven animations are a common UX pattern on the web. A scroll-driven animation is linked to the scroll position of a scroll container. This means that as you scroll up or down, the linked animation scrubs forward or backward in direct response. Examples of this are effects such as parallax background images or reading indicators which move as you scroll.
A similar type of scroll-driven animation is an animation that is linked to an element's position within its scroll container. With it, for example, elements can fade-in as they come into view.
The classic way to achieve these kinds of effects is to respond to scroll events onthe main thread, which leads to two main problems:
- Modern browsers perform scrolling on a separate process and therefore deliver scroll events asynchronously.
- Main thread animations aresubject to jank.
This makes creating performant scroll-driven animations that are in-sync with scrolling impossible or very difficult.
From Chrome version 115 there is a new set of APIs and concepts that you can use to enable declarative scroll-driven animations: Scroll Timelines and View Timelines.
These new concepts integrate with the existingWeb Animations API (WAAPI) andCSS Animations API, allowing them to inherit the advantages these existing APIs bring. That includes the ability to have scroll-driven animations run off the main thread. Yes, read that correctly: you can now have silky smooth animations, driven by scroll, running off the main thread, with just a few lines of extra code. What's not to like?!
Note: If you can’t wait to check out some demos go visithttps://scroll-driven-animations.style, a site packed with demos and tools to check out.Animations on the web, a small recap
Animations on the web with CSS
To create an animation in CSS, define a set of keyframes using the@keyframes
at-rule. Link it up to an element using theanimation-name
property while also setting ananimation-duration
to determine how long the animation should take. There are moreanimation-*
longhand properties available–animation-easing-function
andanimation-fill-mode
just to name a few–which can all be combined in theanimation
shorthand.
For example, here’s an animation that scales up an element on the X-axis while also changing its background color:
@keyframesscale-up{from{background-color:red;transform:scaleX(0);}to{background-color:darkred;transform:scaleX(1);}}#progressbar{animation:2.5slinearforwardsscale-up;}
Animations on the web with JavaScript
In JavaScript, the Web Animations API can be used to achieve exactly the same. You can do this by either creating newAnimation
andKeyFrameEffect
instances, or use the much shorterElement
animate()
method.
document.querySelector('#progressbar').animate({backgroundColor:['red','darkred'],transform:['scaleX(0)','scaleX(1)'],},{duration:2500,fill:'forwards',easing:'linear',});
This visual result of the JavaScript snippet above is identical to the previous CSS version.
Animation timelines
By default, an animation attached to an element runs on thedocument timeline. Its origin time starts at 0 when the page loads, and starts ticking forwards as clock time progresses. This is the default animation timeline and, until now, was the only animation timeline you had access to.
TheScroll-driven Animations Specification defines two new types of timelines that you can use:
- Scroll Progress Timeline: a timeline that is linked to the scroll position of a scroll container along a particular axis.
- View Progress Timeline: a timeline that is linked to the relative position of a particular element within its scroll container.
Scroll Progress Timeline
A Scroll Progress Timeline is an animation timeline that is linked to progress in the scroll position of a scroll container–also calledscrollport orscroller–along a particular axis. It converts a position in a scroll range into a percentage of progress.
The starting scroll position represents 0% progress and the ending scroll position represents 100% progress. In the following visualization, you can see that the progress counts up from 0% to 100% as you scroll the scroller from top to bottom.
✨ Try it for yourself
A Scroll Progress Timeline is often abbreviated to simply “Scroll Timeline”.
View Progress Timeline
This type of timeline is linked to the relative progress of a particular element within a scroll container. Just like a Scroll Progress Timeline, a scroller’s scroll offset is tracked. Unlike a Scroll Progress Timeline, it’s the relative position of a subject within that scroller that determines the progress.
This is somewhat comparable to howIntersectionObserver
works, which can track how much an element is visible in the scroller. If the element is not visible in the scroller, it is not intersecting. If it is visible inside the scroller–even for the smallest part–it is intersecting.
A View Progress Timeline begins from the moment a subject starts intersecting with the scroller and ends when the subject stops intersecting the scroller. In the following visualization, you can see that the progress starts counting up from 0% when the subject enters the scroll container and reaches 100% at the very moment the subject has left the scroll container.
✨ Try it for yourself
A View Progress Timeline is often abbreviated to simply “View Timeline”. It is possible to target specific parts of a View Timeline based on the subject’s size, but more on that later.
Getting practical with Scroll Progress Timelines
Creating an anonymous Scroll Progress Timeline in CSS
The easiest way to create a Scroll Timeline in CSS is to use thescroll()
function. This creates an anonymous Scroll Timeline that you can set as the value for the newanimation-timeline
property.
Example:
@keyframesanimate-it{…}.subject{animation:animate-itlinear;animation-timeline:scroll(rootblock);}
animation-timeline
longhand property is not part of theanimation
shorthand and must be declared separately. Furthermore,animation-timeline
must be declared after theanimation
shorthand as the shorthand will reset non-included longhands to their initial value.Thescroll()
function accepts a<scroller>
and an<axis>
argument.
Accepted values for the<scroller>
argument are the following:
nearest
: Uses the nearest ancestor scroll container(default).root
: Uses the document viewport as the scroll container.self
: Uses the element itself as the scroll container.
Accepted values for the<axis>
argument are the following:
block
: Uses the measure of progress along the block axis of the scroll container(default).inline
: Uses the measure of progress along the inline axis of the scroll container.y
: Uses the measure of progress along the y axis of the scroll container.x
: Uses the measure of progress along the x axis of the scroll container.
For example, to bind an animation to the root scroller on the block axis, the values to pass intoscroll()
areroot
andblock
. Put together, the value isscroll(root block)
.
animation-duration
set in seconds does not make sense when using a Scroll Progress Timeline, you must setanimation-duration
toauto
. Alternatively, as done in the code snippet above, you can omit theanimation-duration
from theanimation
shorthand as it will then use its default value which isauto
.Demo: Reading progress indicator
This demo has a reading progress indicator fixed to the top of the viewport. As you scroll down the page, the progress bar grows until it takes up the full viewport width upon reaching the end of the document. An anonymous Scroll Progress Timeline is used to drive the animation.
✨ Try it for yourself
The reading progress indicator is positioned at the top of the page using position fixed. To leverage composited animations, not thewidth
is being animated but the element is scaled down on the x-axis using atransform
.
<body> <div></div> …</body>
@keyframesgrow-progress{from{transform:scaleX(0);}to{transform:scaleX(1);}}#progress{position:fixed;left:0;top:0;width:100%;height:1em;background:red;transform-origin:050%;animation:grow-progressautolinear;animation-timeline:scroll();}
The timeline for the animationgrow-progress
on the#progress
element is set to an anonymous timeline that’s created usingscroll()
. No arguments are given toscroll()
so it will fall back to its default values.
The default scroller to track is thenearest
one, and the default axis isblock
. This effectively targets the root scroller as that is the nearest scroller of the#progress
element, while tracking its block direction.
Creating a named Scroll Progress Timeline in CSS
An alternative way to define a Scroll Progress Timeline is to use a named one. It’s a bit more verbose, but it can come in handy when you aren’t targeting a parent scroller or the root scroller, or when the page uses multiple timelines or when automatic lookups don’t work. This way, you can identify a Scroll Progress Timeline by the name that you give it.
To create a named Scroll Progress Timeline on an element, set thescroll-timeline-name
CSS property on the scroll container to an identifier of your liking. The value must start with--
.
To tweak which axis to track, also declare thescroll-timeline-axis
property. Allowed values are the same as the<axis>
argument ofscroll()
.
Finally, to link the animation to the Scroll Progress Timeline, set theanimation-timeline
property on the element that needs to be animated to the same value as the identifier used for thescroll-timeline-name
.
Code Example:
@keyframesanimate-it{…}.scroller{scroll-timeline-name:--my-scroller;scroll-timeline-axis:inline;}.scroller.subject{animation:animate-itlinear;animation-timeline:--my-scroller;}
If wanted, you can combinescroll-timeline-name
andscroll-timeline-axis
in thescroll-timeline
shorthand. For example:
scroll-timeline:--my-scrollerinline;
Demo: Horizontal carousel step indicator
This demo features a step indicator shown above each image carousel. When a carousel contains three images, the indicator bar starts at 33% width to indicate you are currently looking at image one of three. When the last image is in view–determined by the scroller having scrolled to the end–the indicator takes up the full width of the scroller. A named Scroll Progress Timeline is used to drive the animation.
✨ Try it for yourself
The base markup for a gallery is this:
<div> <div> <div></div> <div>…</div> <div>…</div> </div></div>
The.gallery__progress
element is absolutely positioned within the.gallery
wrapper element. Its initial size is determined by the--num-images
custom property.
.gallery{position:relative;}.gallery__progress{position:absolute;top:0;left:0;width:100%;height:1em;transform:scaleX(calc(1/var(--num-images)));}
The.gallery__scrollcontainer
lays out the contained.gallery__entry
elements horizontally and is the element that scrolls. By tracking its scroll position, the.gallery__progress
gets animated. This is done by referring to the named Scroll Progress Timeline--gallery__scrollcontainer
.
@keyframesgrow-progress{to{transform:scaleX(1);}}.gallery__scrollcontainer{overflow-x:scroll;scroll-timeline:--gallery__scrollcontainerinline;}.gallery__progress{animation:autogrow-progresslinearforwards;animation-timeline:--gallery__scrollcontainer;}
animation-timeline: scroll(nearest inline)
on.gallery__progress
it would not find the scroller from the.gallery__scrollcontainer
even if that element is its direct parent.The reason for this, is that lookups fornearest
only considers the elements that can affect its position and size. Because.gallery__progress
is absolutely positioned, the first parent element that will determine its size and position is the.gallery
element as it hasposition: relative
applied, thereby jumping over the.gallery__scrollcontainer
element.Expressed in more technical terms, the lookup walks upthe containing block chain to find the nearestscroll container.Creating a Scroll Progress Timeline with JavaScript
To create a Scroll Timeline in JavaScript, create a new instance of theScrollTimeline
class. Pass in a property bag with thesource
andaxis
that you want to track.
source
: A reference to the element whose scroller that you want to track. Usedocument.documentElement
to target the root scroller.axis
: Determines which axis to track. Similar to the CSS variant, accepted values areblock
,inline
,x
, andy
.
consttl=newScrollTimeline({source:document.documentElement,});
To attach it to a Web Animation, pass it in as thetimeline
property and omit anyduration
if there was any.
$el.animate({opacity:[0,1],},{timeline:tl,});
Demo: Reading progress indicator, revisited
To recreate the reading progress indicator with JavaScript, while using the same markup, use the following JavaScript code:
const$progressbar=document.querySelector('#progress');$progressbar.style.transformOrigin='0% 50%';$progressbar.animate({transform:['scaleX(0)','scaleX(1)'],},{fill:'forwards',timeline:newScrollTimeline({source:document.documentElement,}),});
The visual result is identical in the CSS version: the createdtimeline
tracks the root scroller and scale the#progress
up on the x-axis from 0% to 100% as you scroll the page.
✨ Try it for yourself
Getting practical with View Progress Timeline
Creating an Anonymous View Progress Timeline in CSS
To create a View Progress Timeline, use theview()
function. Its accepted arguments are<axis>
and<view-timeline-inset>
.
- The
<axis>
is the same as from the Scroll Progress Timeline and defines which axis to track. The default value isblock
. - With
<view-timeline-inset>
, you can specify an offset(positive or negative) to adjust the bounds when an element is considered to be in view or not. The value must be a percentage orauto
, withauto
being the default value.
For example, to bind an animation to an element intersecting with its scroller on the block axis, useview(block)
. Similar toscroll()
, set this as the value for theanimation-timeline
property and don’t forget to set theanimation-duration
toauto
.
Using the following code, everyimg
will fade-in as it crosses the viewport while you scroll.
@keyframesreveal{from{opacity:0;}to{opacity:1;}}img{animation:reveallinear;animation-timeline:view();}
<scroller>
of a View Timeline, as it always tracks the subject within its nearest parent scroller.Intermezzo: View Timeline ranges
By default, an animation linked to the View Timeline attaches to the entire timeline range. This starts from the moment the subject is about to enter the scrollport and ends when the subject has left the scrollport entirely.
It is also possible to link it to a specific part of the View Timeline by specifying the range that it should attach to. This can be, for example, only when the subject is entering the scroller. In the following visualization, the progress starts counting up from 0% when the subject enters the scroll container but already reaches 100% from the moment it is entirely intersecting.
The possible View Timeline ranges that you can target are the following:
cover
: Represents the full range of the view progress timeline.entry
: Represents the range during which the principal box is entering the view progress visibility range.exit
: Represents the range during which the principal box is exiting the view progress visibility range.entry-crossing
: Represents the range during which the principal box crosses the end border edge.exit-crossing
: Represents the range during which the principal box crosses the start border edge.contain
: Represents the range during which the principal box is either fully contained by, or fully covers, its view progress visibility range within the scrollport. This depends on whether the subject is taller or shorter than the scroller.
To define a range, you must set a range-start and range-end. Each consists of range-name(see list above) and a range-offset to determine the position within that range-name. The range-offset is typically a percentage ranging from0%
to100%
but you can also specify a fixed length such as20em
.
For example, if you want to run an animation from the moment a subject enters, chooseentry 0%
as the range-start. To have it finished by the time the subject has entered, chooseentry 100%
as a value for the range-end.
In CSS, you set this using theanimation-range
property. Example:
animation-range:entry0%entry100%;
In JavaScript, use therangeStart
andrangeEnd
properties.
$el.animate(keyframes,{timeline:tl,rangeStart:'entry 0%',rangeEnd:'entry 100%',});
Use the tool embedded below to see what each range-name represents and how the percentages affect the start and end positions. Try to set the range-start toentry 0%
and the range-end tocover 50%
, and then drag the scrollbar to see the animation result.
Watch a recording
As you might notice while playing around with this View Timeline Ranges tools, some ranges can be targeted by two different range-name + range-offset combinations. For example,entry 0%
,entry-crossing 0%
, andcover 0%
all target the same area.
When the range-start and range-end target the same range-name and span the entire range–from 0% up to 100%–you can shorten the value to simply the range name. For example,animation-range: entry 0% entry 100%;
can be rewritten to the much shorteranimation-range: entry
.
scale
andtranslate
are not taken into account when deriving the ranges. This is a good thing, as this allows you to scale a subject during scroll without affecting the available scroll estate. If the transformed box were used, attached animations would flicker because they would constantly need to be recalculated in response to a change in scroll estate.Demo: Image reveal
This demo fades in the images as they enter the scrollport. This is done using an Anonymous View Timeline. The animation range has been tweaked so that each image is at full opacity when it is halfway the scroller.
✨ Try it for yourself
The expanding effect is achieved by using a clip-path that is animated. The CSS used for this effect is this:
@keyframesreveal{from{opacity:0;clip-path:inset(0%60%0%50%);}to{opacity:1;clip-path:inset(0%0%0%0%);}}.revealing-image{animation:autolinearrevealboth;animation-timeline:view();animation-range:entry25%cover50%;}
Creating a named View Progress Timeline in CSS
Similar to how Scroll Timelines have named versions, you can also create named View Timelines. Instead of thescroll-timeline-*
properties you use variants that carry theview-timeline-
prefix, namelyview-timeline-name
andview-timeline-axis
.
The same type of values apply, and the same rules for looking up a named timeline apply.
Demo: Image reveal, revisited
Reworking the image reveal demo from earlier, the revised code looks like this:
.revealing-image{view-timeline-name:--revealing-image;view-timeline-axis:block;animation:autolinearrevealboth;animation-timeline:--revealing-image;animation-range:entry25%cover50%;}
Usingview-timeline-name: revealing-image
, the element will be tracked within its nearest scroller. The same value is then used as the value for theanimation-timeline
property. The visual output is exactly the same as before.
✨ Try it for yourself
Creating a View Progress Timeline in JavaScript
To create a View Timeline in JavaScript, create a new instance of theViewTimeline
class. Pass in a property bag with thesubject
that you want to track,axis
, andinset
.
subject
: A reference to the element that you want to track within its own scroller.axis
: The axis to track. Similar to the CSS variant, accepted values areblock
,inline
,x
, andy
.inset
: An inset(positive) or outset(negative) adjustment of the scrollport when determining whether the box is in view.
consttl=newViewTimeline({subject:document.getElementById('subject'),});
To attach it to a Web Animation, pass it in as thetimeline
property and omit anyduration
if there was any. Optionally, pass in range information using therangeStart
andrangeEnd
properties.
$el.animate({opacity:[0,1],},{timeline:tl,rangeStart:'entry 25%',rangeEnd:'cover 50%',});
✨ Try it for yourself
Note: The animated element$el
and thesubject
do not need to be the same element. This means that you can track an element in its scroller while animating a distant element somewhere else in the DOM tree.More things to try out
Attaching to multiple View Timeline ranges with one set of keyframes
Let’s take a look at this contact list demo where the list entries are animated. As a list entry enters the scrollport from the bottom it slides+fades in, and as it exits the scrollport at the top it slides+fades out.
✨ Try it for yourself
For this demo, each element gets decorated with one View Timeline that tracks the element as it crosses its scrollport yet two scroll-driven animations are attached to it. Theanimate-in
animation is attached to theentry
range of the timeline, and theanimate-out
animation to theexit
range of the timeline.
@keyframesanimate-in{0%{opacity:0;transform:translateY(100%);}100%{opacity:1;transform:translateY(0);}}@keyframesanimate-out{0%{opacity:1;transform:translateY(0);}100%{opacity:0;transform:translateY(-100%);}}#list-viewli{animation:animate-inlinearforwards,animate-outlinearforwards;animation-timeline:view();animation-range:entry,exit;}
Instead of running two different animations attached to two different ranges, it is also possible to create one set of keyframes that already contains the range information.
@keyframesanimate-in-and-out{entry0%{opacity:0;transform:translateY(100%);}entry100%{opacity:1;transform:translateY(0);}exit0%{opacity:1;transform:translateY(0);}exit100%{opacity:0;transform:translateY(-100%);}}#list-viewli{animation:linearanimate-in-and-out;animation-timeline:view();}
As the keyframes contain the range information, you don’t need to specify theanimation-range
. The result is exactly the same as it was before.
✨ Try it for yourself
Attaching to a non-ancestor Scroll Timeline
Note: The feature described in this section is not supported in Chrome 115. To try it out, use Chrome 116 with the Experimental Web Platform Features flag enabled.The lookup mechanism for named Scroll Timelines and named View Timelines is limited to scroll ancestors only. Very often though, the element that needs to be animated is not a child of the scroller that needs to be tracked.
To make this work, thetimeline-scope
property comes into play. You use this property to declare a timeline with that name without actually creating it. This gives the timeline with that name a broader scope. In practice, you use thetimeline-scope
property on a shared parent element so that a child scroller’s timeline can attach to it.
For example:
.parent{timeline-scope:--tl;}.parent.scroller{scroll-timeline:--tl;}.parent.scroller~.subject{animation:animatelinear;animation-timeline:--tl;}
In this snippet:
- The
.parent
element declares a timeline with the name--tl
. Any child of it can find and use it as a value for theanimation-timeline
property. - The
.scroller
element actually defines a Scroll Timeline with the name--tl
. By default it would only be visible to its children but because.parent
has it set as thescroll-timeline-root
, it attaches to it. - The
.subject
element uses the--tl
timeline. It walks up its ancestor tree and finds--tl
on the.parent
. With the--tl
on the.parent
pointing to the--tl
of.scroller
, the.subject
will essentially track the.scroller
’s Scroll Progress Timeline.
Put differently, you can usetimeline-root
to move a timeline up to an ancestor (akahoisting), so that all children of the ancestor can access it.
Thetimeline-scope
property can be used with both both Scroll Timelines and View Timelines.
More demos and resources
All demos covered in this article onthe scroll-driven-animations.style mini-site. The website includes many more demos to highlight what is possible with Scroll-driven animations.
One of the additional demos is this list of album covers. Each cover rotates in 3D as it takes the center spotlight.
✨ Try it for yourself
Or this stacking cards demo that leverageposition: sticky
. As the cards stack, the already stuck cards scale down, creating a nice depth effect. In the end, the entire stack slides out of view as a group.
✨ Try it for yourself
Also featured onscroll-driven-animations.style is a collection of tools such as the View Timeline Range Progress visualization that was included earlier in this post.
Scroll-driven animations are also covered inWhat’s new in Web Animations at Google I/O ’23.
Except as otherwise noted, the content of this page is licensed under theCreative Commons Attribution 4.0 License, and code samples are licensed under theApache 2.0 License. For details, see theGoogle Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.
Last updated 2023-05-05 UTC.