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"

機能検出

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-timelineScrollTrigger
標準×
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%",
    }
  )
})

イベント受信

失敗パターン

症状対処
ScrollTimeline が undefined非対応ブラウザ。polyfill
polyfill 重い本当に必要な要素にだけ適用
animation が cancel されないcleanup を必ず
進行度が 0 のままsource / subject の対象が間違い
CSS で済むなら CSS で

JS が要るのは「動的に生成」動的に切替」「他の状態と組み合わせ」のとき。 静的なリビールや進捗バーは CSS で書くのが軽くて読みやすい。