CSS heart animated on scroll with CSS

Animating elements while a user scrolls the page is a popular practice on the web. With scroll-timeline and the scroll function available in Chrome Canary, we get a nice preview on how these scrolling animations in pure CSS will work. The perfect time to create some demos and play around with it.

If you want to try the demo’s, you should open them in Chrome Canary and navigate to chrome://flags. Inside of the settings, enable the Experimental Web Platform features.

It’s time for some new properties again! Nothing too complicated to start with. Let’s take the following animation into account:

.animate-on-scroll {
  animation: fadeIn 1s ease-out;

@keyframes fadeIn {
   0% {
	opacity: 0;
   100% {
       opacity: 1;

If you want to make this CSS animation run only when a user scrolls the page. Just add the following:

.animate-on-scroll {
  animation-timeline: scroll(root);

That was easy right? A few things are happening now. The percentage of our keyframes is the same percentage that the user scrolls. So in this case we are fading something from 0 to 1 over the whole length of a page scroll.

But what is this scroll() function we see here? This is something new and it’s very exciting.

The scroll function

The scroll function can have two values inside of it. The first part specifies whether the scroll port we’re targeting is the root scroller or the nearest ancestor. We can also specify the scroll direction in which the animation should occur. Options are: horizontal, vertical and logical variants block and inline. The default of this function is the following:

.animate-on-scroll {
  animation-timeline: scroll(block nearest);

It’s a really handy feature, especially for targeting the root scroller of a page. Maybe we want to show a little “scroll to top”-button when the user scrolls about 30% of the page’s length. This could be done easily without using JavaScript:

<main id="top">
  <!-- some content here -->
  <a href="#top" class="scroll-to-top">
    Scroll to top

And we can reveal our scroll to top button while scrolling simply by doing the following:

.scroll-to-top {
  animation: revealScroller 1s ease-out;
  animation-timeline: scroll(root);

@keyframes revealScroller {
  30% {
    opacity: 0;
    transform: translateY(100%);
  100% {
    opacity: 1;
    transform: translateY(0);

When the user scrolled 30% of the page, the button will reveal itself and will be fully visible by the time the user scrolled 38% of the page. Here is the demo of this:

I had a lot of fun playing around with this. Another useful example that many websites seem to have is a progress bar to indicate how much you’ve read of a certain article. I created my own little version of this with a little "chicky" walking on your screen while scrolling, be warned though, it has a bit of keyframe overload:

The downside is that my animation seems to trigger a bit of moonwalking when scrolling back to top. Let’s just call it a feature for now, ok? 😉

Naming a scroll-timeline

if the animated element doesn’t need to interact with the nearest ancestor or root scroll but rather with a scroll happening somewhere else on our page, we have the ability to target a custom scroll-timeline. As a simple example, take a look at the following HTML:

  <div class="list">
      <li>Ducks, geese, and waterfowl</li>
      <li>Pheasants, grouse, and allies</li>
      <!-- etc etc etc -->
  <!-- item we want to animate -->
  <div class="animation"></div>

In this example, .list has an overflow-y: auto; and a fixed height. When we scroll in our .list we want .animation to do something. In this case, .animation isn’t a direct child, so we will use a scroll-timeline-name to name our .list:

.list {
  scroll-timeline: listTimeline vertical;

scroll-timeline is a shorthand for:

  • scroll-timeline-name: provides a name to the selector
  • scroll-timeline-axis: The direction of the scroll: horizontal, vertical, block or inline.

By naming this list we can now bind our .animation selector by declaring the animation-timeline again, but now using the name instead of the scroll function:

.animation {
  position: absolute;
  top: 0;
  right: 0;
  animation: moveBackground 1s alternate linear;
  animation-timeline: listTimeline;
  /* some repeating bg image */

@keyframes moveBackground {
  0% {
    background-position: 0 0;
  100% {
    background-position: 0 100px;

These are two results of using these techniques. One for the vertical scroll and another for a horizontal scroll:

Checking for support for scroll-timeline

We can use a feature query to enable our animations only for those browsers that support it. It’s a good idea to do this as I noticed that your animation will just fire right away if the feature isn’t supported.

@supports (scroll-timeline: works) {
/* cool scrolly stuff goes here */

When using this in our projects, it's nice to check for user preferences if you’re animating a lot of things on scroll, so check if a user doesn't want a lot of distractions because of the motion. For those people add the following media feature query:

@media (prefers-reduced-motion) {
  /* cancel the animations here */

I didn’t use this in my demo’s, but lately, I always make sure to do this for heavy-animated elements in projects.

Conclusion and final demo

There are still a lot of requests and questions being asked on the CSSWG github and it shows that this is only the beginning. Personally, I'm wondering if we will be able to target the scroll direction so that my Chicky demo won’t have to moonwalk back into position (So, I asked the question, can’t hurt to try). As it stands at the moment, this won’t be a complete solution for those big scroll animation heavy websites that use libraries such as GSAP, but in my opinion, it doesn’t have to be. The ability to trigger these little animations and create some extra interaction is a great addition to CSS. But I'm sure many more demo’s will be made soon with people going crazy on these things.

And because I’m creating this as my valentine's day article, let me share some love for the scroll with this extra demo:

 in  CSS , UX