Web Animations APIのanimateメソッドで実現するアニメーション
Web Animations API(WAAPI)のelement.animate()は、CSSアニメーションと同じ性能で、JavaScriptから開始・停止・逆再生・完了待ちができる唯一の標準APIです。全モダンブラウザで使えます。
使うべき場面もはっきりしています。「アニメーションの終了を待って次の処理をしたい」「値を実行時に計算したい」「途中で止めたり巻き戻したりしたい」——このどれかに当てはまるならWAAPI、そうでなければCSSで十分です。
この記事では基本構文から、finishedによる完了待ち、fillとcommitStyles()の落とし穴、そして実用的な波紋エフェクトの実装までを扱います。
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 |
再生時間(ミリ秒) | 300〜600 |
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('全部表示された');
delayにi * 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.leftとrect.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()で焼き付ける- 動かすのは
transformとopacity。width/heightはカクつく - 座標を使うときは
getBoundingClientRect()で基準を合わせる getAnimations()でCSS由来のアニメーションもまとめて制御できるprefers-reduced-motionはJavaScript側で判定する。duration: 0に倒すのが安全
単純な動きはCSSに任せ、「終わったら次を実行する」が必要になった時点でWAAPIに切り替える。この基準で選ぶと、コードが無駄に複雑になりません。