Web Animations API(WAAPI)のelement.animate()は、CSSアニメーションと同じ性能で、JavaScriptから開始・停止・逆再生・完了待ちができる唯一の標準APIです。全モダンブラウザで使えます。

使うべき場面もはっきりしています。「アニメーションの終了を待って次の処理をしたい」「値を実行時に計算したい」「途中で止めたり巻き戻したりしたい」——このどれかに当てはまるならWAAPI、そうでなければCSSで十分です。

この記事では基本構文から、finishedによる完了待ち、fillcommitStyles()の落とし穴、そして実用的な波紋エフェクトの実装までを扱います。

animate() の基本構文

element.animate(keyframes, options)の形で呼び、戻り値のAnimationオブジェクトで制御します。

const box = document.querySelector('.box');

const animation = box.animate(
  [
    { transform: 'translateX(0px)',   opacity: 1 },
    { transform: 'translateX(100px)', opacity: 0.5 },
  ],
  {
    duration: 1000,
    easing: 'ease-out',
    iterations: 1,
  }
);

 

option 意味 よく使う値
duration 再生時間(ミリ秒) 300600
easing 加減速 'ease-out''cubic-bezier(...)'
delay 開始までの待ち時間
iterations 繰り返し回数 Infinityで無限
direction 再生方向 'alternate'で往復
fill 終了後の状態 'forwards'(後述の注意あり)
pseudoElement 擬似要素を対象にする '::before'

キーフレームは2つの書き方がある

配列で書く方法(上)のほかに、プロパティごとに配列を渡す書き方もあります。後者の方が読みやすい場面が多いです。

box.animate(
  {
    transform: ['translateX(0px)', 'translateX(100px)'],
    opacity:   [1, 0.5],
    offset:    [0, 1],   // 各キーフレームの位置(0〜1)
    easing:    ['ease-in'], // キーフレーム間ごとの指定も可能
  },
  { duration: 1000 }
);

 

offsetを使うと、途中の時点を明示できます。「最初の30%で一気に動き、残りでゆっくり戻る」といった調整がしやすくなります。

 

再生・一時停止・逆再生

返ってきたAnimationオブジェクトで、CSSアニメーションでは難しい制御ができます。

const animation = box.animate(
  [{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],
  { duration: 2000, iterations: Infinity }
);

document.getElementById('pause').onclick   = () => animation.pause();
document.getElementById('play').onclick    = () => animation.play();
document.getElementById('reverse').onclick = () => animation.reverse();

// 速度を変える(0.5で半分、2で倍速)
document.getElementById('slow').onclick = () => { animation.playbackRate = 0.5; };

// 好きな時点にジャンプする
animation.currentTime = 1000; // 1秒地点

// 完全に破棄して初期状態に戻す
document.getElementById('reset').onclick = () => animation.cancel();

 

finish()cancel()の違いに注意してください。finish()は最終状態まで飛ばし、cancel()は開始前の状態に戻します。

 

完了を待つ:finished プロパティ

WAAPIを使う最大の理由がこれです。 animation.finishedは Promise なので、awaitでアニメーションの完了を待てます。setTimeoutで時間を推測する必要がありません。

async function fadeOutAndRemove(el) {
  const animation = el.animate(
    [{ opacity: 1 }, { opacity: 0 }],
    { duration: 300, easing: 'ease-out' }
  );

  await animation.finished;
  el.remove();
}

 

複数のアニメーションの完了をまとめて待つこともできます。

const items = document.querySelectorAll('.item');

const animations = [...items].map((el, i) =>
  el.animate(
    [{ opacity: 0, transform: 'translateY(12px)' }, { opacity: 1, transform: 'none' }],
    { duration: 400, delay: i * 60, fill: 'backwards' }
  )
);

await Promise.all(animations.map((a) => a.finished));
console.log('全部表示された');

 

delayi * 60を掛けるだけで、順番に現れるスタッガーアニメーションになります。CSSでこれをやるには、要素の数だけanimation-delayを書く必要がありました。

ただし注意点があります。cancel()するとfinishedは reject されます。 途中で中断する可能性があるなら、必ずcatchしてください。

try {
  await animation.finished;
  el.remove();
} catch (err) {
  // cancel() された。何もしない
}

 

fill: ‘forwards’ の落とし穴と commitStyles()

アニメーション後の状態を保持したいとき、素直にfill: 'forwards'を指定しがちですが、これには問題があります。アニメーションのオブジェクトがメモリに残り続け、その要素のスタイルを他のCSSより優先し続けるのです。

要素を大量に扱う画面では、これが積み重なって重くなります。正しい書き方はcommitStyles()で結果をインラインスタイルに焼き付けてからcancel()することです。

const animation = box.animate(
  [{ transform: 'none' }, { transform: 'translateX(100px)' }],
  { duration: 500, fill: 'forwards' }
);

await animation.finished;

animation.commitStyles(); // 最終状態を style 属性に書き出す
animation.cancel();       // アニメーションを破棄する

 

これでboxにはstyle="transform: translateX(100px)"が付き、アニメーション自体は消えます。以後は通常のCSSとして上書きできます。

 

実践:波紋(リップル)エフェクト

クリック位置から円が広がるエフェクトを、WAAPIで実装します。ポイントは3つあります。

  • width/heightではなくtransform: scale()を動かす:サイズを動かすとレイアウトが再計算されてカクつきます
  • 座標の基準を合わせるclientXはビューポート基準なので、position: absoluteとは噛み合いません
  • 後片付けはfinishedで行うsetTimeoutで時間を二重管理しない
.ripple-host {
  position: relative;
  overflow: hidden; /* 円がはみ出さないように */
}

.ripple {
  position: absolute;
  border-radius: 50%;
  background: rgb(255 255 255 / .5);
  pointer-events: none;
  width: 100px;
  height: 100px;
  will-change: transform, opacity;
}

 

const SIZE = 100;

document.querySelectorAll('.ripple-host').forEach((host) => {
  host.addEventListener('pointerdown', async (event) => {
    // 動きを減らす設定なら何もしない
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;

    const rect = host.getBoundingClientRect();
    const ripple = document.createElement('span');
    ripple.className = 'ripple';

    // host を基準にした座標に変換する
    ripple.style.left = `${event.clientX - rect.left - SIZE / 2}px`;
    ripple.style.top  = `${event.clientY - rect.top  - SIZE / 2}px`;

    host.appendChild(ripple);

    const animation = ripple.animate(
      [
        { transform: 'scale(0)',   opacity: 0.6 },
        { transform: 'scale(2.5)', opacity: 0   },
      ],
      { duration: 500, easing: 'cubic-bezier(.2,.7,.4,1)' }
    );

    try {
      await animation.finished;
    } finally {
      ripple.remove();
    }
  });
});

 

rect.leftrect.topを引いているのが座標変換です。これを忘れると、ページをスクロールした状態でクリックしたときに、円が見当違いの場所に出ます

また、イベントはclickではなくpointerdownにしています。指を置いた瞬間に反応する方が、体感の反応速度が上がります。

 

既存のアニメーションを取得する

getAnimations()を使うと、CSSで定義したアニメーションやトランジションも含めて取得・制御できます。

// この要素で動いている全アニメーションを止める
el.getAnimations().forEach((a) => a.cancel());

// ページ全体(CSSアニメーション含む)の完了を待つ
await Promise.all(
  document.getAnimations().map((a) => a.finished.catch(() => {}))
);

 

画面遷移の前に「動いているものを全部止める」といった処理が、これ1行で書けます。catchを挟んでいるのは、途中でキャンセルされた分でPromise.all全体が失敗しないようにするためです。

 

CSSアニメーションとどう使い分けるか

要件 選ぶもの
ホバーや状態変化の単純な動き CSS(transition
常時ループする装飾 CSS(@keyframes
完了を待って次の処理をしたい WAAPI(finished
値を実行時に計算する(クリック座標など) WAAPI
一時停止・逆再生・速度変更 WAAPI
要素の出現に合わせた演出 Intersection Observer + CSS
スクロール量に追従する動き CSS(animation-timeline

要素が画面に入ったときの演出についてはIntersection Observer APIを使ったスクロールアニメーションの実装方法にまとめています。

 

アクセシビリティへの配慮

WAAPIはCSSの@media (prefers-reduced-motion: reduce)の影響を受けません。JavaScript側で明示的に判定する必要があります。

const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)');

function animateSafely(el, keyframes, options) {
  // 動きを減らす設定なら、時間を0にして最終状態だけ適用する
  const duration = reduceMotion.matches ? 0 : options.duration;
  return el.animate(keyframes, { ...options, duration });
}

 

duration: 0にすれば、アニメーションは無くなるが最終状態には到達するので、処理の流れを壊さずに済みます。「動かさない=何も起きない」にしてしまうと、完了待ちの処理が止まる原因になります。

 

まとめ

  • element.animate(keyframes, options)Animationオブジェクトが返る
  • animation.finishedで完了を待てるのがWAAPI最大の利点。setTimeoutで時間を二重管理しない
  • cancel()するとfinishedは reject される。catchを忘れない
  • fill: 'forwards'で放置せず、commitStyles()cancel()で焼き付ける
  • 動かすのはtransformopacitywidth/heightはカクつく
  • 座標を使うときはgetBoundingClientRect()で基準を合わせる
  • getAnimations()でCSS由来のアニメーションもまとめて制御できる
  • prefers-reduced-motionはJavaScript側で判定する。duration: 0に倒すのが安全

単純な動きはCSSに任せ、「終わったら次を実行する」が必要になった時点でWAAPIに切り替える。この基準で選ぶと、コードが無駄に複雑になりません。

ABOUT ME
りん
このブログでは、Web開発やプログラミングに関する情報を中心に、私が日々感じたことや学んだことをシェアしています。技術と生活の両方を楽しめるブログを目指して、日常で触れた出来事や本、グルメの話題も取り入れています。気軽に覗いて、少しでも役立つ情報や楽しいひとときを見つけてもらえたら嬉しいです。