Published: February 26, 2026
Scroll-driven animations have evolved from janky, main-thread JavaScript implementations to smooth, accessible, off-main-thread experiences using modern CSS and UI features like Scroll Timelines and View Timelines. This shift enables quick prototyping and high-performance animations, while enabling teams to create polished, scrollytelling pages as demonstrated in this article.
NRK and storytelling
NRK (Norwegian Broadcasting Corporation) is the public service broadcaster in Norway. 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 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.

Scroll-driven animations in the browser
Story-driven animation
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
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.
.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.
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.
<h1 aria-label="sjuke">
<span>s</span><span>j</span><span>u</span><span>k</span><span>e</span>
<h1>
h1 span {
display: inline-block;
}
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
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.
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
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.
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 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 animations
- mp4
- three.js
<canvas>
The following example uses layers with KeyframeEffects
:
<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:
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.

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 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.
@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 and ViewTimeline, NRK uses an open-source polyfill, which has an active community contributing to it.
Currently, the polyfill is loaded conditionally when ScrollTimeline
is not
available and using a stripped down version of the polyfill without CSS support.
if (!('ScrollTimeline' in window)) {
await import('scroll-timeline.js')
}
Browser support detection and handling in 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,
--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
and
requestAnimationFrame
in JavaScript like so:
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
- Demos: Scroll-driven Animations
- Animate elements on scroll with Scroll-driven animations
- Codelab: Getting started with scroll-driven animations in CSS
- Chrome Extension: Scroll-driven animation debugger
- Scroll-timeline Polyfill
- Report a bug or new feature? We want to hear from you.
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.