JavaScript / WAAPI で使う
ScrollTimeline / ViewTimeline コンストラクタ、Element.animate() 連携、 polyfill、動的なタイムライン生成。
ScrollTimeline
const timeline = new ScrollTimeline({
source: document.documentElement, // または特定の scroller
axis: "block", // block / inline / x / y
})
document.querySelector(".bar").animate(
[
{ transform: "scaleX(0)" },
{ transform: "scaleX(1)" },
],
{
timeline,
fill: "both",
}
)
ViewTimeline
const target = document.querySelector(".card")
const timeline = new ViewTimeline({
subject: target,
axis: "block",
inset: ["100px", "100px"],
})
target.animate(
[
{ opacity: 0, transform: "translateY(40px)" },
{ opacity: 1, transform: "translateY(0)" },
],
{
timeline,
fill: "both",
rangeStart: { rangeName: "entry", offset: CSS.percent(0) },
rangeEnd: { rangeName: "cover", offset: CSS.percent(30) },
}
)
名前付きタイムラインを参照
const animations = element.getAnimations()
animations.forEach(a => console.log(a.timeline))
// CSS 側で scroll-timeline-name: --my-scroll;
// JS でも同じ名前で参照可能(仕様策定中)
WAAPI の API(おさらい)
const anim = el.animate(keyframes, {
duration: 1000,
timeline: scrollTimeline,
fill: "both",
iterations: 1,
easing: "linear",
rangeStart: ...,
rangeEnd: ...,
})
anim.pause()
anim.play()
anim.cancel()
anim.reverse()
anim.currentTime = 500
anim.playbackRate = 2
anim.finished.then(() => console.log("done"))
動的にタイムラインを切り替え
// 既存の animation の timeline を差し替え
const anim = el.animate(keyframes, { fill: "both" })
anim.timeline = new ScrollTimeline({ source: scroller })
進行度を読む
const tl = new ScrollTimeline({ source: document.documentElement })
// 進行度(0〜1)
console.log(tl.currentTime) // CSSUnitValue
const progress = tl.currentTime.value / tl.duration.value
console.log(progress)
polyfill
非対応ブラウザでも動かすため、Web の scroll-timeline polyfill がある:
npm install scroll-timeline-polyfill
// アプリ起動時に
import "scroll-timeline-polyfill"
- JS で scroll イベントを拾って実装するため、ネイティブよりは重い
- API は同じなので、コードは変更不要
- 非対応な機能(一部 inset 等)もあり完全互換ではない
機能検出
if (CSS.supports("animation-timeline: scroll()")) {
// ネイティブ対応
} else {
// フォールバック(IntersectionObserver / scroll イベント)
}
// JS API
if (typeof ScrollTimeline !== "undefined") {
// 使える
}
動的に @keyframes 生成
const keyframes = [
{ transform: "translateY(0)", opacity: 0 },
{ transform: "translateY(-30px)", opacity: 1, offset: 0.7 },
{ transform: "translateY(0)", opacity: 1 },
]
el.animate(keyframes, { timeline: viewTimeline, fill: "both" })
scrolltimeline と React
useEffect の中で animate を呼んで cleanup でキャンセル:
function ScrollFade({ children }) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!ref.current) return
const tl = new ViewTimeline({ subject: ref.current })
const anim = ref.current.animate(
[{ opacity: 0, transform: "translateY(40px)" }, { opacity: 1, transform: "translateY(0)" }],
{ timeline: tl, fill: "both" }
)
return () => anim.cancel()
}, [])
return <div ref={ref}>{children}</div>
}
motion-dev の use-scroll-driven 系
framer-motion / motion はuseScroll / useTransform でスクロール連動を提供。 ネイティブ scroll-driven が実装されたあとも、framer 側はそのまま使える(互換)。
GSAP ScrollTrigger との比較
| animation-timeline | ScrollTrigger | |
|---|---|---|
| 標準 | ○ | × |
| JS なし | ○ | × |
| パフォーマンス | ◎ (GPU) | ○ |
| 機能の柔軟性 | ○ | ◎ |
| ピン留め / コールバック | 限定的 | ○ |
| 対応ブラウザ | 新しめ | 広い |
典型的なネイティブ実装
1. Reading Progress(JS なしが正解だが、JS で書く例)
const bar = document.querySelector(".progress")
const tl = new ScrollTimeline({ source: document.documentElement })
bar.animate(
{ transform: ["scaleX(0)", "scaleX(1)"] },
{ timeline: tl, fill: "both" }
)
2. 全カードに一括適用
document.querySelectorAll(".reveal").forEach(el => {
const tl = new ViewTimeline({ subject: el })
el.animate(
[{ opacity: 0, transform: "translateY(40px)" }, { opacity: 1, transform: "translateY(0)" }],
{
timeline: tl,
fill: "both",
rangeStart: "entry 0%",
rangeEnd: "cover 30%",
}
)
})
イベント受信
- scroll-driven animation には専用イベントはまだ少ない
- 進入 / 退出を捕まえたいならIntersectionObserver と併用
失敗パターン
| 症状 | 対処 |
|---|---|
| ScrollTimeline が undefined | 非対応ブラウザ。polyfill |
| polyfill 重い | 本当に必要な要素にだけ適用 |
| animation が cancel されない | cleanup を必ず |
| 進行度が 0 のまま | source / subject の対象が間違い |
CSS で済むなら CSS で
JS が要るのは「動的に生成」「動的に切替」「他の状態と組み合わせ」のとき。 静的なリビールや進捗バーは CSS で書くのが軽くて読みやすい。