![Johannes Odland](https://web.dev/images/authors/johannesodland.jpg) Johannes Odland [Mastodon](https://front-end.social/@johannes) [Homepage](https://www.nrk.no/forfatter/johannes-odland-18.21019) ![Harry Theodoulou](https://web.dev/images/authors/harrytheodoulou.jpg) Harry Theodoulou [GitHub](https://github.com/charisTheo) [LinkedIn](https://www.linkedin.com/in/harrytheo) [Homepage](https://www.harrytheo.com/)

<br />


Published: February 26, 2025

<br />

[Scroll-driven animations](https://developer.mozilla.org/docs/Web/CSS/animation-timeline)
have evolved from janky, main-thread JavaScript implementations to smooth,
accessible, off-main-thread experiences using
[modern CSS and UI features](https://developer.chrome.com/blog/css-ui-ecommerce.md.txt)
like Scroll Timelines and View Timelines. This shift enables quick prototyping and
high-performance animations, while enabling teams to create polished,
[scrollytelling](https://en.m.wiktionary.org/wiki/scrollytelling) pages as
demonstrated in this article.

> [!NOTE]
> **Note:** To learn how to implement scroll-driven animations in your own site using the new APIs [watch this video course on YouTube](https://goo.gle/learn-scroll-driven-animations) or read the [Animate elements on scroll with Scroll-driven animations](https://developer.chrome.com/docs/css-ui/scroll-driven-animations.md.txt) article by [Bramus](https://developer.chrome.com/authors/bramus.md.txt).

## NRK and storytelling

[NRK](https://www.nrk.no/) (Norwegian Broadcasting Corporation) is the [public
service broadcaster in
Norway](https://www.nrk.no/about/a-gigantic-small-broadcaster-1.3698462). The
team behind the implementation described in this article is called Visuelle
Historier in Norwegian, translating roughly to *Visual Stories* in English. The
team works with design, graphics, and development for editorial projects for TV,
radio, and the web, developing visual identities, content graphics, feature
articles and new visual storytelling formats. The team also works with NRK's
design profile and sub-brands, creating tools and templates to make it easier to
publish content in line with NRK's brand identity.

## How NRK uses scroll-driven animations

Scroll-driven and scroll-triggered animations enhance their storytelling
articles by making them more interactive, engaging, and memorable. This approach
is especially useful in non-fiction narratives where few or no images are
available.

These animations help strengthen or create dramaturgical points, propel the
story forward, and develop small visual narratives that align with or reinforce
the text. By being scroll-driven, these animations allow the user to control the
progression of the narrative through their scrolling.

### Elevating the user experience

NRK's user insights reveal that readers appreciate how these animations guide
their focus. By highlighting text or animations as they scroll, users find it
easier to identify key points and understand the most important aspects of the
story, especially when skimming.

Additionally, animating graphics can simplify complex information, making it
easier for users to comprehend relationships and changes over time. By building,
adding, or highlighting information dynamically, NRK can present content in a
more pedagogical and engaging way.

### Setting a mood

Animations can be powerful tools for setting or enhancing the mood of a story.
By adjusting the timing, speed, and style of animations, NRK can evoke emotions
that match the narrative's tone.

### Break up text and provide visual relief

NRK often uses small animated illustrations to break up long blocks of text in
the form of a simple dinkus or a small illustration, giving readers a momentary
pause from the narrative. Many users appreciate the variation, noting that it
breaks up the text and makes it more digestible. They feel that it provides a
welcome pause in the narrative.

### Respecting accessibility needs and user preferences

NRK's public pages must be accessible to all citizens of Norway. Therefore, the
pages must respect the user's preference for reduced motion. All page content
must be available to users who have enabled this browser setting.

## Designing scroll-driven animations

NRK has streamlined the design workflow by developing and integrating a new
scroll animation tool directly into their [Sanity](https://www.sanity.io/)
Content Management System (CMS). Developed in collaboration between the teams
that develop and maintain the site and the CMS solutions, this tool allows
designers to easily prototype and implement scroll animations with visual cues
for start and end positions of an animated element, and the ability to preview
animations in real-time. This innovation gives designers greater control and
accelerates the design process directly within the CMS.
![Showing the area that has scrolled into view in a tool.](https://developer.chrome.com/static/blog/nrk-casestudy/image/image1.png) [Similar
example](https://scroll-driven-animations.style/tools/view-timeline/ranges/) of visual cues for start and end positions of animated elements---not the real CMS tool.

## Scroll-driven animations in the browser

### Story-driven animation

[The man who wasn't missed](https://www.nrk.no/stor-oslo/xl/mann-la-dod-i-leilighet-i-oslo-i-ni-ar-1.15337692).

This article about a man who remained dead in his apartment for nine years had to rely heavily on illustrations due to the lack of other visual elements. The illustrations were animated through scrolling to underscore the narrative, such as in the animation wherein the night falls, lights in a multi-storey building turn on progressively until only one apartment remains unlit. The animation was built using NRK's in-house scroll-driven animation tool.

### Text fade animation

[Permafrost](https://www.nrk.no/dokumentar/xl/frostens-vokter-1.14755370).

This article begins with a brief introduction, mirroring the opening sequence of a film. Concise texts paired with full-screen visuals were designed to hint at the article's content, building anticipation to encourage readers to delve into the full piece. The title page was crafted to resemble a film poster, with scroll-driven animations employed to reinforce this sensation by smoothly animating the text up and out.

```css
.article-section {
  animation: fade-up linear;
  animation-timeline: view();
  animation-range: entry 100% exit 100%;
}
```

### Scroll-animated typography

Animated typography in the title of an article---[Sick leave](https://www.nrk.no/norge/skyhogt-sjukefravaer_-sjekk-kor-sjuke-folk-pa-din-alder-er-1.17143974).

With the introduction in "Sjukt sjuke" (which roughly translates to "Sickly sick") NRK wanted to draw readers into an article about the increasing sick leave rates in Norway. The title was meant to be a visual eye-catcher that gives readers a hint that this is not the usual, boring, number-driven story they might expect. The NRK team wanted the text and illustrations to play on the themes of the piece, using typography and scroll-driven animations to enhance this. The article utilizes NRK News' new font and design profile.

```html
<h1 aria-label="sjuke">
  <span>s</span><span>j</span><span>u</span><span>k</span><span>e</span>
<h1>
```

```css
h1 span {
  display: inline-block;
}
```

```javascript
if (window.matchMedia('print, (prefers-reduced-motion: reduce)').matches) {
  return;
}

const heading = document.querySelector("h1");
const letters = heading.querySelectorAll("span");

const timeline = new ViewTimeline({ subject: heading });
const scales = [/**/];
const rotations = [/**/];

for ([index, el] of letters.entries()) {
  el.animate(
    {
      scale: ["1", scales[index]],
      rotate: ["0deg", rotations[index]]
    },
    {
      timeline,
      fill: "both",
      rangeStart: "contain 30%",
      rangeEnd: "contain 70%",
      easing: "ease-out"
    }
  );
}
```

### Highlighting scroll-snapped items

[Kids in institutions](https://www.nrk.no/stor-oslo/barn-og-unge-i-barnevernet-deler-videoer-om-rus-pa-tiktok-1.16835047).

Readers who have finished an article often want to read more about the same issue. In the articles about youth abusing substances in institutions, NRK wanted to recommend a single article as the next read, while also giving readers the option of several others if they so wished. The solution was a swipeable navigation implemented with scroll snap and scroll-driven animations. The animations ensured that the active element was in focus, while the remaining elements were dimmed down.

```javascript
for (let item of items) {
  const timeline = new ViewTimeline({ subject: item, axis: "inline" });
  const animation = new Animation(effect, timeline);
  item.animate(
    {
      opacity: [0.3, 1, 0.3]
    },
    { timeline, easing: "ease-in-out", fill: "both" }
  );
  animation.rangeStart = "cover calc(50% - 100px)";
  animation.rangeEnd = "cover calc(50% + 100px)";
}
```

### Scroll-animation triggering a regular animation

[Budget](https://www.nrk.no/norge/statsbudsjettet-er-lagt-fram_-slik-vil-staten-bruka-skattepengane-dine-1.17055718#:%7E:text=Me%20startar%20med%20det%20du%20brukar%20mest%20pengar%20p%C3%A5%3A).

In this article about Norway's national budget, NRK aimed to make an otherwise heavy and dull number-based story more accessible and personalized. The goal was to break down an enormous and incomprehensible budget figure and give the reader a personal reckoning of what their tax money is being spent on. Each sub-section focused on a specific item in the national budget. The reader's total tax contribution was symbolized with a blue bar that was divided up to reveal the reader's contribution to these individual items. The transition was achieved with a scroll-driven animation that triggered the individual items to be animated in.

```javascript
const timeline = new ViewTimeline({
  subject: containerElement
});

// Setup scroll-driven animation
const scrollAnimation = containerElement.animate(
  {
    "--cover-color": ["blue", "lightblue"],
    scale: ["1 0.2", "1 3"]
  },
  {
    timeline,
    easing: "cubic-bezier(1, 0, 0, 0)",
    rangeStart: "cover 0%",
    rangeEnd: "cover 50%"
  }
);

// Wait for scroll-driven animation to complete
await scrollAnimation.finished;
scrollAnimation.cancel();

// Trigger time-driven animations
for (let [index, postElement] of postElements.entries()) {
  const animation = postElement?.animate(
    { scale: ["1 3", "1 1"] },
    {
      duration: 200,
      delay: index * 33,
      easing: "ease-out",
      fill: "backwards"
    }
  );
}
```
> "We've done scroll-driven animation for a long time. Before the Web Animations API existed, we had to use scroll events, later combined with the Intersection Observer API. This was often a very time consuming task, and now this is made trivial by the Web Animations and Scroll-Driven Animations APIs"---*Helge Silset, Front-end Developer at NRK*

NRK has many different [Web
Components](https://web.dev/articles/web-components-io-2019) that can be plugged
into one of their custom elements, called `ScrollAnimationDriver`
(`<scroll-animation-driver>`), supporting the following animations:

- Layers with `[KeyframeEffects](https://developer.mozilla.org/docs/Web/API/KeyframeEffect)`
- [Lottie](https://airbnb.io/lottie/#/) animations
- mp4
- [three.js](https://threejs.org/)
- `<canvas>`

The following example uses layers with `KeyframeEffects`:

```html
<scroll-animation-driver data-range-start='entry-crossing 50%' data-range-end='exit-crossing 50%'>
  <layered-animation-effect>
    <picture>
      <source />
      <img />
    </picture>

    <picture>
      <source />
      <img />
    </picture>

    <picture>
      <source />
      <img />
    </picture>
  </layered-animation-effect>
</scroll-animation-driver>
```

NRK's JavaScript implementation of their `<scroll-animation-driver>` custom
element:

```javascript
export default class ScrollAnimationDriver extends HTMLElement {
  #timeline

  connectedCallback() {
    this.#timeline = new ViewTimeline({subject: this})
    for (const child of this.children) {
      for (const effect of child.effects ?? []) {
        this.#setupAnimationEffect(effect)
      }
    }
  }

  #setupAnimationEffect(effect) {
    const animation = new Animation(effect, this.#timeline) 
    animation.rangeStart = this.rangeStart
    animation.rangeEnd = this.rangeEnd

    if (this.prefersReducedMotion) {
      animation.currentTime = CSS.percent(this.defaultProgress * 100)
    } else {
      animation.play()
    }
  }
}

export default class LayeredAnimationEffect extends HTMLElement {
  get effects() {
    return this.layers.flatMap(layer => toKeyframeEffects(layer))
  }
}
```

### Scrolling performance

NRK had a very performant JavaScript implementation before scroll-driven
animations were used, but now scroll-driven animations allow them to have even
better performance without having to worry about scroll jank, even on
low-powered devices.

- Non-SDA task duration: 1 ms.
- SDA task duration: 0.16 ms.

> [!NOTE]
> **Note:** The budget for painting a new frame in order to achieve 60 Frames Per Second (FPS) is 16.7 ms---Learn more about [Why are some animations slow?](https://web.dev/articles/animations-overview)

![The Performance tab of Chrome DevTools.](https://developer.chrome.com/static/blog/nrk-casestudy/image/image2.png) The recording in the Performance tab of Chrome DevTools with a 6 times CPU slowdown shows 0.16ms for each task in a new frame.

To read more about the difference in scrolling performance between JavaScript
implementations versus scroll-driven animations, the article
[A case study on scroll-driven animations performance](https://developer.chrome.com/blog/scroll-animation-performance-case-study.md.txt)
goes into more detail.

## Accessibility and UX considerations

Accessibility plays an important role in NRK's public pages as they have to be
accessible to all of Norway's citizens under many circumstances. NRK makes sure
scroll animations are accessible in a few different ways:

- **Respecting user's preferences for reduced motion** : Using media query `screen and (prefers-reduced-motion: no-preference)` to apply the animation as a progressive enhancement. It's also helpful to handle print styles at the same time.
- **Considering the wide range of devices and varying scroll input
  precision**: Some users may scroll in steps (Space or up/down keys, navigating to landmarks using a screen reader) and not see the entire animation. Make sure crucial information isn't missed.
- **Being cautious with animations that show or hide content**: For users relying on Operating System (OS) zoom, it can be difficult to notice that hidden content will appear as they scroll. Avoid making users search for it. If hiding or showing content is necessary, ensure consistency in where it appears and disappears.
- **Avoiding large changes in brightness or contrast in the animation**: Since scroll-driven animations depend on user control, abrupt luminance shifts can appear as flashing, which may trigger seizures for some users.

```css
@media (prefers-reduced-motion: no-preference) {
  .article-image {
    opacity: 0;
    transition: opacity 1s ease-in-out;
  }
  .article-image.visible {
    opacity: 1;
  }
}
```

## Browser support

For wider browser support of the
[ScrollTimeline](https://developer.mozilla.org/docs/Web/API/ScrollTimeline)
and
[ViewTimeline](https://developer.mozilla.org/docs/Web/API/ViewTimeline),
NRK uses an [open-source polyfill](https://github.com/flackr/scroll-timeline),
which has an [active community contributing to
it](https://github.com/flackr/scroll-timeline/graphs/contributors).

Currently, the polyfill is loaded conditionally when `ScrollTimeline` is not
available and using a stripped down version of the polyfill without CSS support.

```javascript
if (!('ScrollTimeline' in window)) {
  await import('scroll-timeline.js')
}
```

Browser support detection and handling in CSS:

```css
@supports not (animation-timeline: view()) {
  .article-section {
    translate: 0 calc(-15vh * var(--fallback-progress));
    opacity: var(--fallback-progress);
  }
}

@supports (animation-timeline: view()) {
  .article-section {
    animation: --fade-up linear;
    animation-timeline: view();
    animation-range: entry 100% exit 100%;
  }
}
```

In the previous example for unsupported browsers, NRK is using a [CSS
variable](https://developer.mozilla.org/docs/Web/CSS/Using_CSS_custom_properties),
`--fallback-progress`, as a fallback to controlling the animation timeline for
the `translate` and `opacity` properties.

The `--fallback-progress` CSS variable is then updated with a [`scroll` event
listener](https://developer.mozilla.org/docs/Web/API/Document/scroll_event)
and
[`requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/Window/requestAnimationFrame)
in JavaScript like so:

```javascript
function updateProgress() {
  const end = el.offsetTop + el.offsetHeight;
  const start = end - window.innerHeight;
  const scrollTop = document.scrollingElement.scrollTop;
  const progress = (scrollTop - start) / (end - start);
  document.body.style.setProperty('--fallback-progress', clamp(progress, 0, 1));
}


if (!CSS.supports("animation-timeline: view()")) {
  document.addEventListener('scroll', () => {
    if (!visible || updating) {
      return;
    }

    window.requestAnimationFrame(() => {
      updateProgress();
      updating = false;
    });

    updating = true;
  });
}
```

## Resources

- [Scroll-driven animations case studies](https://developer.chrome.com/blog/css-ui-ecommerce-sda.md.txt)
- [Demos: Scroll-driven Animations](https://scroll-driven-animations.style/)
- [Animate elements on scroll with Scroll-driven animations](https://developer.chrome.com/docs/css-ui/scroll-driven-animations.md.txt)
- [Codelab: Getting started with scroll-driven animations in CSS](https://codelabs.developers.google.com/scroll-driven-animations#0)
- [Chrome Extension: Scroll-driven animation debugger](https://chromewebstore.google.com/detail/scroll-driven-animations/ojihehfngalmpghicjgbfdmloiifhoce)
- [Scroll-timeline Polyfill](https://github.com/flackr/scroll-timeline?tab=readme-ov-file#scroll-timeline-polyfill)
- [Report a bug or new feature? We want to hear from you](https://issues.chromium.org/issues/new?component=1456613&template=0).

*Special thanks to Hannah Van Opstal, Bramus, and Andrew Kean Guan from Google,
and Ingrid Reime from NRK for their valuable contributions to this work.*

*NRK reserves all rights to its company name, logo and the articles to which links are provided and/or quotes are derived.*