Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Scroll-driven animations

A regular animation runs by the clock: two seconds — and it is over. But its progress can be tied not to time but to scrolling: scroll the page halfway — the animation is halfway through, scroll back — it rewinds. This used to be written in JavaScript; now one CSS property is enough.

A scroll timeline: scroll()

The animation-timeline property sets the timeline — what drives the animation. The scroll() function links it to the scrolling of the nearest scrollable block. Let us make a reading progress bar — a strip that grows as the text is scrolled:

<style>
  @keyframes grow {
    from { scale: 0 1; }
    to { scale: 1 1; }
  }
  .reader { height: 180px; overflow-y: auto; border: 1px solid silver; }
  .progress {
    position: sticky;
    top: 0;
    height: 6px;
    background: tomato;
    transform-origin: left;
    animation: grow linear;
    animation-timeline: scroll();
  }
  .reader p { margin: 12px; }
</style>
 
<div class="reader">
  <div class="progress"></div>
  <p>Scroll this text — the red strip at the top shows how much you have read.</p>
  <p>Espresso is made under pressure: hot water passes through tightly packed ground coffee in about half a minute.</p>
  <p>A cappuccino is espresso, milk and thick milk foam in roughly equal parts.</p>
  <p>A latte has more milk and a thin foam, so it tastes milder.</p>
  <p>A flat white is made with a double espresso and a thin layer of velvety milk.</p>
  <p>You have read to the end — the strip is full.</p>
</div>

No duration is given: with a scroll timeline, its role is played by the path from the start to the end of scrolling. On your own page give the bar position: fixed — then scroll() finds the scrolling of the whole page.

A view timeline: view()

The view() function follows not the whole scroll but how the element itself passes through the visible area. This is how blocks are revealed on scroll:

<style>
  @keyframes reveal {
    from { opacity: 0; translate: -60px 0; }
    to { opacity: 1; translate: 0 0; }
  }
  .list { height: 200px; overflow-y: auto; padding: 8px; border: 1px solid silver; }
  .item {
    margin-bottom: 12px;
    padding: 24px 12px;
    background: lavender;
    animation: reveal linear both;
    animation-timeline: view();
    animation-range: entry;
  }
</style>
 
<div class="list">
  <div class="item">Cappuccino</div>
  <div class="item">Latte</div>
  <div class="item">Mocha</div>
  <div class="item">Flat white</div>
  <div class="item">Espresso</div>
  <div class="item">Americano</div>
</div>

Each card slides in from the left while it appears from below. Scroll back — the animation plays in reverse. both in animation keeps the first frame before the start and the last one after the end.

The range: animation-range

animation-range chooses on which part of the element's path the animation runs. In the example above entry means while the card is entering the visible area:

RangeWhen the animation runs
coverthe whole time the element is at least partly visible (default)
containwhile the element is fully visible
entrywhile the element is entering the visible area
exitwhile the element is leaving it

Percentages can be added: entry 20% cover 50% — from 20% of the entry to the middle of the path.

Support and a fallback

Scroll-driven animations work in Chrome and Edge since 2023, Safari added them later; check current support, especially in Firefox, on caniuse.com. If elements appear from transparency, in an old browser they may stay invisible. So such styles are wrapped in @supports:

@supports (animation-timeline: view()) {
  .item {
    animation: reveal linear both;
    animation-timeline: view();
    animation-range: entry;
  }
}

Do not forget prefers-reduced-motion from the lesson "Animations" either: for people who turned off motion, simply showing the content is enough.

Common mistake

animation-timeline before animation. The animation shorthand resets all its parts, the timeline included, back to time. An animation with a duration of 0 ends instantly: the bar is full at once and does not change:

<style>
  @keyframes grow { from { scale: 0 1; } to { scale: 1 1; } }
  .reader { height: 120px; overflow-y: auto; border: 1px solid silver; }
  .bar { position: sticky; top: 0; height: 6px; background: crimson; transform-origin: left;
    animation-timeline: scroll();
    animation: grow linear; }
  .reader p { margin: 12px; height: 200px; }
</style>
 
<div class="reader">
  <div class="bar"></div>
  <p>Scroll — the bar does not change.</p>
</div>

Write animation first, and animation-timeline and animation-range after it.

Thanks for reading!

Donate to the site — we work hard for you.