發布日期:2026 年 2 月 26 日
捲動驅動動畫已從笨拙的主執行緒 JavaScript 實作,轉變為使用新式 CSS 和 UI 功能 (例如捲動時間軸和檢視時間軸) 的流暢、易用的非主執行緒體驗。這項轉變可讓團隊快速製作原型和高效能動畫,同時製作出精緻的捲動式敘事頁面,如本文所示。
NRK 和說故事
NRK (挪威廣播公司) 是挪威的公共廣播服務廣播公司。本文所述實作項目的團隊在挪威文中稱為 Visuelle Historier,大致對應的英文為 Visual Stories。這個團隊會為電視、廣播和網路的編輯專案提供設計、圖像和開發服務,開發視覺識別、內容圖像、特寫文章和新的視覺敘事格式。這個團隊也與 NRK 的設計師和子品牌合作,製作工具和範本,讓你更輕鬆地發布符合 NRK 品牌形象的內容。
NRK 如何使用捲動驅動動畫
透過捲動驅動和捲動觸發的動畫,讓內容更具互動性、更吸引人,並讓讀者更容易記住內容。這種做法特別適合用於非虛構敘事,因為這類內容通常只有少數或完全沒有圖片。
這些動畫有助於強化或創造戲劇性重點、推進故事情節,並發展出與文字相符或加強文字的短小敘事。這些動畫是透過捲動操作驅動,讓使用者可透過捲動操作控制敘述內容的進度。
提升使用者體驗
NRK 的使用者洞察資料顯示,讀者很喜歡這些動畫引導他們專注於內容。在使用者捲動畫面時,系統會標出文字或動畫,方便他們找出重點,並瞭解故事中最關鍵的部分,尤其是在瀏覽時。
此外,動畫圖形可簡化複雜資訊,讓使用者更容易理解關係和隨時間變化的情形。NRK 可透過動態建立、新增或強調資訊,以更具教育意義且更吸引人的方式呈現內容。
設定情境
動畫是設定或強化故事情緒的強大工具。調整動畫的時間、速度和風格,NRK 可喚起與敘事基調相符的情緒。
分割文字並提供視覺緩衝
NRK 經常使用小型動畫插圖,以簡單的插圖或小插圖的形式分隔長篇文字,讓讀者暫時停下來觀看敘述。許多使用者都很喜歡這種變化,認為這有助於分割文字,讓內容更易於消化。他們認為這能讓敘述內容有個適當的停頓。
尊重無障礙需求和使用者偏好
NRK 的公開網頁必須可供所有挪威公民存取。因此,網頁必須尊重使用者減少動畫的偏好設定。所有網頁內容都必須向已啟用這項瀏覽器設定的使用者開放。
設計捲動驅動動畫
NRK 開發並直接將新的捲動動畫工具整合至 Sanity 內容管理系統 (CMS),藉此簡化設計工作流程。這項工具是由開發及維護網站和 CMS 解決方案的團隊共同開發,可讓設計人員輕鬆製作原型,並透過視覺提示實作捲動動畫,以便針對動畫元素的開始和結束位置,以及即時預覽動畫。這項創新功能可讓設計師進一步控管,並直接在 CMS 中加速設計流程。

瀏覽器中的捲動驅動動畫
以故事為主軸的動畫
這篇文章報導一名男子在公寓內死亡九年,由於缺乏其他視覺元素,因此必須大量使用插圖。插圖會透過捲動進行動畫處理,以強調敘事內容。例如在夜晚降臨的動畫中,多層建築的燈光會逐漸亮起,直到只剩一間公寓未亮燈為止。這部動畫是使用 NRK 的內部捲動動畫工具建構而成。
文字淡出動畫
永久凍土。
本文開頭會簡單介紹內容,就像電影的開場片段。簡潔的文字搭配全螢幕圖像,旨在暗示文章內容,營造期待感,鼓勵讀者深入瞭解整篇文章。標題頁的設計類似電影海報,透過捲動驅動的動畫效果,讓文字以流暢的動畫效果向上和向外移動,強化這種感覺。
.article-section {
animation: fade-up linear;
animation-timeline: view();
animation-range: entry 100% exit 100%;
}
捲動動畫排版
在「Sjukt sjuke」(大致翻譯為「Sickly sick」) 的介紹中,NRK 希望吸引讀者閱讀一篇關於挪威病假率上升的文章。標題的用意是吸引讀者目光,讓他們知道這不是他們預期的平淡無趣、以數字為主的內容。NRK 團隊希望文字和插圖能配合作品的主題,並透過字體排版和捲動式動畫加以強化。這篇文章使用 NRK News 的新字型和設計設定檔。
<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"
}
);
}
醒目顯示捲動對齊的項目
讀完文章後,通常會想進一步瞭解相關議題。在關於機構內青少年濫用藥物的文章中,NRK 希望推薦一篇文章供讀者閱讀,並提供其他幾篇文章供讀者選擇。解決方案是使用捲動貼齊和捲動驅動動畫實作的滑動式導覽功能。動畫可確保聚焦於有效元素,並將其他元素調暗。
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)";
}
捲動動畫觸發一般動畫
預算:
在這篇關於挪威國家預算的文章中,NRK 希望讓原本沉重且枯燥的數字故事,變得更易於閱讀且更貼近讀者。目的是將龐大且難以理解的預算數字細分,讓讀者瞭解自己的稅金用於何處。每個子部分都聚焦於國家預算中的特定項目。讀者的總稅收貢獻以藍色長條圖表示,並分成多個部分,揭露讀者對這些個別項目的貢獻。這項轉場效果是透過捲動驅動動畫達成,該動畫會觸發個別項目的動畫效果。
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"
}
);
}
「我們很早就開始使用捲動驅動動畫,在 Web Animations API 推出之前,我們必須使用捲動事件,後來再搭配 Intersection Observer API。這項作業通常耗時甚久,但現在只要使用 Web Animations 和 Scroll-Driven Animations API,就能輕鬆完成。」Helge Silset,NRK 的前端開發人員
NRK 提供許多不同的網頁元件,可插入其中一個名為 ScrollAnimationDriver
(<scroll-animation-driver>
) 的自訂元素,支援下列動畫:
- 使用
[KeyframeEffects](https://developer.mozilla.org/docs/Web/API/KeyframeEffect)
的圖層 - Lottie 動畫
- mp4
- three.js
<canvas>
以下範例使用含有 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 的 <scroll-animation-driver>
自訂元素 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))
}
}
捲動效能
在使用捲動驅動動畫之前,NRK 就已實作效能極佳的 JavaScript,但現在捲動驅動動畫可讓效能更上一層樓,無須擔心捲動時的卡頓情形,即使是在低耗電裝置上也一樣。
- 非 SDA 任務的時間長度:1 毫秒。
- SDA 工作時間:0.16 毫秒。

如要進一步瞭解 JavaScript 實作和捲動驅動動畫之間的捲動效能差異,請參閱「捲動驅動動畫效能之個案研究」一文。
無障礙和使用者體驗考量
在許多情況下,NRK 的公開網頁必須讓所有挪威公民都能存取,因此無障礙設計在這些網頁中扮演重要角色。NRK 會確保捲動動畫可透過以下幾種方式存取:
- 尊重使用者減少動畫的偏好設定:使用媒體查詢
screen and (prefers-reduced-motion: no-preference)
將動畫套用為漸進式增強功能。同時處理列印樣式也很有幫助。 - 考量裝置種類繁多,且捲動輸入精確度各異:部分使用者可能會以步驟 (空白鍵或上/下鍵、使用螢幕閱讀器前往地標) 捲動,而不會看到整個動畫。確保不會遺漏重要資訊。
- 謹慎使用顯示或隱藏內容的動畫:如果使用者依賴作業系統 (OS) 縮放功能,可能很難注意到隱藏的內容會在捲動時顯示。避免讓使用者搜尋。如果需要隱藏或顯示內容,請確保內容的顯示和消失位置一致。
- 避免動畫中的亮度或對比度有大幅變化:由於捲動驅動的動畫取決於使用者控制,突然的亮度變化可能會出現閃爍效果,進而引發部分使用者的癲癎症狀。
@media (prefers-reduced-motion: no-preference) {
.article-image {
opacity: 0;
transition: opacity 1s ease-in-out;
}
.article-image.visible {
opacity: 1;
}
}
瀏覽器支援
為了讓更多瀏覽器支援 ScrollTimeline 和 ViewTimeline,NRK 使用了開放原始碼 polyfill,並有活躍社群提供貢獻。
目前,當 ScrollTimeline
無法使用,且使用沒有 CSS 支援的簡化版 polyfill 時,會以條件式方式載入 polyfill。
if (!('ScrollTimeline' in window)) {
await import('scroll-timeline.js')
}
瀏覽器支援在 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%;
}
}
在前述針對不支援的瀏覽器的範例中,NRK 使用 CSS 變數 --fallback-progress
做為備用方案,用於控制 translate
和 opacity
屬性的動畫時間表。
接著,使用 scroll
事件事件監聽器和 JavaScript 中的 requestAnimationFrame
更新 --fallback-progress
CSS 變數,如下所示:
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;
});
}
資源
- 捲動驅動動畫個案研究
- 示範:捲動驅動動畫
- 使用捲動驅動動畫為捲動中的元素加上動畫效果
- 程式碼研究室:開始使用 CSS 中的捲動驅動動畫
- Chrome 擴充功能:捲動驅動動畫偵錯工具
- 捲動時間軸填充
- 要回報錯誤或新功能嗎?我們希望能聽聽你的看法。
特別感謝 Google 的 Hannah Van Opstal、Bramus 和 Andrew Kean Guan,以及 NRK 的 Ingrid Reime,感謝他們對這項工作做出寶貴的貢獻。