Featured image of post A CSS-Only Scroll-Driven Animation Effect

A CSS-Only Scroll-Driven Animation Effect

Using sticky and animation-timeline to achieve element-fixed scroll-driven animations

This article is published on my personal website, original link: A CSS-Only Scroll-Driven Animation Effect

First, let’s look at this simple example

Code:

You can also scroll down to see the effect directly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<div class="out-cont">
    <!-- The outermost element, used to define the fixed distance of the element -->
    <div class="inner-cont">
        <!-- Inner element, used to fix the position of the element -->
        <div class="animation-item">
            <!-- The content of the element that plays the animation -->
        </div>
    </div>
</div>
<style>
.out-cont {
/* The outermost element, used to define the fixed distance of the element, which means the element will be fixed for two screen heights */
    width: 100%;
    height: 200vh;
    position: relative;
}
.inner-cont {
/* Inner element, used to fix the position of the element, using sticky to fix the element within the range */
    width: 100%;
    height: 50vh;
    position: sticky;
    top: 25vh;
    display: flex;
    align-items: center;
    justify-content: center;
}
.animation-item{
/* The content of the element that plays the animation, using animation-timeline to achieve scroll-driven animation */
    width: 20%;
    aspect-ratio: 1;
    background-color: #2E74B5;
    animation: move 1s linear forwards;
    animation-timeline: view();
    animation-range: contain;
}
@keyframes move {
    0% {
        transform: rotate(0deg);
        border-radius: 8%;
    }
    50% {
        transform: rotate(360deg);
        border-radius: 50%;
    }
    100% {
        border-radius: 8%;
        transform: rotate(720deg);
    }
}
</style>

Effect:

.out-cont
.inner-cont
Viewport

sticky: Sticky Layout

For front-end developers, this is not unfamiliar, so I’ll just briefly introduce it: position: sticky is a layout method between position:relative and position:fixed. When the parent element appears on the screen, it behaves like fixed, fixing itself on the screen. When the parent element goes out of the screen, it behaves like relative, following the normal document flow layout, being taken away by the parent container. Regarding how this case uses sticky to achieve similar element fixing effects, there is a simple demonstration on the right side of the effect example above, which can help with understanding. And this demonstration is also pure CSS.

All parent elements of a sticky element cannot set overflow:hidden, as this will invalidate sticky. If really needed, you can use overflow:clip instead.

animation-timeline: Scroll-Driven Animation

This is the core of this case. When this property is used on an element, the CSS animation defined by @keyframes will not play automatically, but will scroll according to the progress of the scrollbar. This property has two main values:

  • animation-timeline: view() The animation starts when the element enters the viewport and ends when it leaves the viewport.
  • animation-timeline: scroll() The animation plays when the element scrolls within the entire scroll container.
  • animation-timeline: cont-name Named container, which will be discussed later.
Viewport
view()
Viewport
scroll()

animation-timeline: scroll()

scroll() is for the entire page and is relatively simple. It is used for effects related to global scrolling, such as article reading progress. Here I quote a picture from 前端侦探.

scroll() can take two parameters: scroller and axis

  • scroller The scroller parameter is used to specify the scroll container, with the default value being nearest. If set to nearest, the nearest ancestor scroll container will be used. If set to root, the document viewport will be used as the scroll container. If set to self, the element itself will be used as the scroll container.

  • axis The axis parameter is used to specify the scroll axis, with the default value being block. If set to block, the block-level axis direction of the scroll container. If set to inline, the inline axis direction of the scroll container. If set to x, the element will scroll horizontally. If set to y, the element will scroll vertically.

animation-range

If I don’t want the animation to play throughout the entire scrolling period, I can use the animation-range property to set the start and end positions of the animation, with px and % units both acceptable. For example:

1
2
3
.animation{
    animation-range: 0 100px;
}

This way, the animation will only play when the scroll container scrolls to the position of 0 to 100px, and will not play after 100px.

animation-timeline: view()

view() is relative to the position of the element and the viewport, and this case is based on view(). view() can also take two parameters: axis and inset

  • axis The axis parameter is used to specify the scroll axis, with the default value being block. If set to block, the block-level axis direction of the scroll container. If set to inline, the inline axis direction of the scroll container. If set to x, the element will scroll horizontally. If set to y, the element will scroll vertically.

  • inset The inset parameter is used to specify when the animation starts and ends, whether it starts when the element just peeks out or when the element completely enters the viewport, which is controlled by this property, somewhat similar to the role of animation-range above. inset accepts one or two values. When there are two values, they represent the start position and end position, with px and % units both acceptable.

animation-timeline: cont-name

You may have noticed that with the above properties alone, we can only achieve animations where the parent element scrolls to drive the child element. What if I need to scroll one container to drive the animation of another sibling element? Then we need to rely on named containers. It’s very simple to use. Just use an attribute scroll-timeline-name on the scroll container.

1
2
3
4
5
6
7
8
.scroll{
    /* Naming the scroll container */
    scroll-timeline-name: --my-scroller;
}
.animation{
    /* The element that needs to be driven by the animation */
    animation-timeline: --my-scroller;
}

In this way, when the .scroll container is scrolled, the .animation element will play the animation according to the scroll progress of the scroll container.

position:fixed animation-timeline animation-range 前端侦探

Licensed under CC BY-NC-SA 4.0
Last updated on Sep 14, 2025 00:00 UTC
Built with Hugo
Theme Stack designed by Jimmy