「ボタンを押したらパネルを開く」「クリックした場所まで要素を飛ばす」といった動きを CSS の @keyframes だけで書こうとすると、移動量をあらかじめ決め打ちしたり、再生・停止のためにクラスを付け外ししたり、と回りくどくなりがちです。element.animate() を使えば、アニメーションそのものを JavaScript で組み立てて、再生・一時停止・逆再生・速度変更まで手元で操作できます。この記事では、キーフレームの2通りの書き方、options に指定できる項目、戻り値の Animation オブジェクトによる制御、終了後の処理の書き方、そして CSS アニメーションとの使い分けまでを順に解説します。
目次
element.animate() は JavaScript でアニメーションを組み立てる関数
element.animate() は、Web Animations API(WAAPI)というブラウザー標準の仕組みの入り口になるメソッドです。要素に対して呼ぶと、その場でアニメーションが始まり、戻り値として Animation オブジェクトが返ってきます。CSS ファイルに @keyframes を書いておく必要はなく、アニメーションの定義も再生の制御も JavaScript のなかで完結します。
const animation = element.animate( keyframes, options );
第1引数の keyframes が「どう変化させるか」、第2引数の options が「どう再生するか」を表します。CSS でいえば keyframes が @keyframes のブロック、options が animation-duration などのプロパティ群にあたると考えると分かりやすいでしょう。そして戻り値の Animation オブジェクトが、CSS にはなかった「あとから操作する」ための取っ手になります。
まずは最小のコードで動かしてみる
要素を1秒かけて右へ移動させながら、少しずつ透明にする例です。
const box = document.querySelector('.box');
box.animate(
[
{ transform: 'translateX(0)', opacity: 1 }, // 開始時の状態
{ transform: 'translateX(200px)', opacity: 0 }, // 終了時の状態
],
1000 // 1000ミリ秒 = 1秒かけて変化させる
);
第2引数に数値だけを渡すと、それが duration(再生時間・ミリ秒)として扱われます。細かい指定が要らないときはこの書き方がいちばん短くて済みます。
覚えておきたいのは、animate() を呼んだ時点で再生が始まることです。あとから play() を呼ぶ必要はありません。また、既定では終了した瞬間にスタイルが元の見た目へ戻ります。移動した先にとどめたい場合は、あとで説明する fill オプションが必要になります。
キーフレームの書き方は2通りある
第1引数のキーフレームには、配列で書く形式とオブジェクトで書く形式の2つがあります。どちらで書いても結果は同じなので、扱う内容に合うほうを選べば大丈夫です。
配列形式:時点ごとの状態を並べる
配列の要素ひとつが「ある時点での見た目」を表します。何も指定しなければ、各キーフレームは再生時間のなかに等間隔で割り当てられます。要素が3つなら 0%・50%・100% の位置です。
box.animate(
[
{ transform: 'translateY(0)' }, // 0%
{ transform: 'translateY(-40px)' }, // 50%
{ transform: 'translateY(0)' }, // 100%
],
{ duration: 600 }
);
等間隔ではなく好きな位置に置きたいときは、キーフレームのなかに offset を書きます。値は 0〜1 の割合で、CSS の @keyframes でいう 0%〜100% にあたります。さらに easing をキーフレームごとに書くと、そのキーフレームから次のキーフレームまでの変化にだけ緩急が効きます。
box.animate(
[
{ transform: 'translateY(0)', easing: 'ease-out' }, // 0% → 30% の区間に効く
{ transform: 'translateY(-40px)', offset: 0.3 }, // 30% の時点
{ transform: 'translateY(0)' }, // 100% の時点
],
{ duration: 600 }
);
オブジェクト形式:プロパティごとに値を並べる
もうひとつは、プロパティ名をキーにして、その値の変化を配列で書く形式です。プロパティの数が少ないときは、こちらのほうが短く書けます。
box.animate(
{
transform: ['translateY(0)', 'translateY(-40px)', 'translateY(0)'],
opacity: [1, 0.4, 1],
offset: [0, 0.3, 1], // それぞれの値を置く位置
easing: ['ease-out', 'ease-in'], // 区間ごとの緩急
},
{ duration: 600 }
);
この形式では offset と easing もまとめて配列で渡します。プロパティごとに配列の長さが違っていても構わず、それぞれの配列が再生時間のなかに等間隔で割り当てられます。
プロパティ名はキャメルケースで書く
どちらの形式でも、CSS プロパティ名は background-color ではなく backgroundColor のようにキャメルケースで書きます。JavaScript のオブジェクトのキーとして扱うためです。値は '200px' や '#ff7043' のように単位や記号まで含めた文字列で指定し、opacity のように単位を持たないプロパティだけ数値のまま書けます。
options でタイミングと繰り返しを決める
第2引数にオブジェクトを渡すと、再生時間だけでなく繰り返しや向きまで細かく指定できます。名前と意味は CSS の animation-* プロパティとほぼ対応しているので、CSS アニメーションを書いたことがあればすぐに読めるはずです。
| オプション | 既定値 | 説明 |
|---|---|---|
duration | 0 | 再生にかける時間(ミリ秒)。指定しないと一瞬で終わってしまうので、実質必須の項目 |
easing | 'linear' | 変化の緩急。'ease-in-out'・'cubic-bezier(.4, 0, .2, 1)'・'steps(4, end)' など CSS と同じ書き方 |
delay | 0 | 再生を始めるまでの待ち時間(ミリ秒) |
iterations | 1 | 繰り返す回数。Infinity で無限にくり返す。2.5 のような小数も指定できる |
direction | 'normal' | 再生の向き。'reverse' は逆向き、'alternate' は往復、'alternate-reverse' は逆向きから始める往復 |
fill | 'auto' | 再生時間外のスタイルの扱い。'forwards' で終了後も最後の状態を保ち、'backwards' で delay 中に最初の状態を先取りし、'both' で両方。既定の 'auto' は 'none' と同じふるまい |
endDelay | 0 | 変化が終わってから「終了」と見なされるまでの待ち時間(ミリ秒)。次のアニメーションへつなぐときの間合いに使う |
iterationStart | 0 | 何周目のどこから始めるか。0.5 なら1周目の途中から再生される |
混同しやすいのが delay と endDelay です。delay は再生前の待ち時間で、体感としては「少し置いてから動き出す」動きになります。対する endDelay は変化が終わったあとの待ち時間で、見た目は止まったままですが、そのアニメーションが終了したことになるタイミングが後ろにずれます。
また fill は、初心者が最初につまずきやすい項目です。既定のままだと、アニメーションが終わった瞬間にスタイルが元の状態へ戻ります。移動先やフェードアウトした状態をそのまま保ちたい場合は、fill: 'forwards' を指定してください。
戻り値の Animation オブジェクトで再生を操作する
ここからが element.animate() の本領です。戻り値の Animation オブジェクトを変数に受け取っておけば、あとから好きなタイミングで止めたり、巻き戻したり、速度を変えたりできます。よく使うメソッドとプロパティは次のとおりです。
| メソッド・プロパティ | できること |
|---|---|
play() | 再生する。すでに終了している場合は先頭に戻って再生し直す |
pause() | その場で一時停止する。play() で続きから再開できる |
reverse() | 再生方向を反転して再生する(再生中なら巻き戻しになる) |
finish() | 再生方向の終端まで一気に飛ばして終了させる |
cancel() | アニメーションを取り消し、適用していたスタイルを取り除く |
currentTime | 現在の再生位置(ミリ秒)。代入すると、その時点まで移動できる |
playbackRate | 再生速度。1 が等倍、2 で2倍速、負の値にすると逆再生になる |
playState | いまの状態。'idle' / 'running' / 'paused' / 'finished' のいずれか |
finished | アニメーションが終わったときに解決するプロミス |
onfinish | 終了したときに呼ばれるイベントハンドラー |
実際にボタンから操作してみると、どのメソッドが何をするのかがつかみやすくなります。次のデモは往復するアニメーションを4回くり返すもので、各ボタンから Animation のメソッドを呼び、状態(playState と currentTime)をリアルタイムに表示しています。
<div class="track">
<div class="box">BOX</div>
</div>
<div class="controls">
<button id="play">再生</button>
<button id="pause">一時停止</button>
<button id="reverse">逆再生</button>
<button id="finish">最後まで飛ばす</button>
<button id="cancel">キャンセル</button>
</div>
<label class="rate">再生速度
<select id="rate">
<option value="0.5">0.5倍</option>
<option value="1" selected>1倍</option>
<option value="2">2倍</option>
</select>
</label>
<p id="status">status</p>
body {
font-family: sans-serif;
margin: 16px;
}
.track {
overflow: hidden;
padding: 12px;
border: 1px dashed #ccc;
border-radius: 8px;
}
.box {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: #007bff;
color: #fff;
font-size: 12px;
}
.controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
}
button {
padding: 6px 12px;
border: 1px solid #007bff;
border-radius: 6px;
background: #fff;
color: #007bff;
cursor: pointer;
}
button:hover {
background: #eaf3ff;
}
.rate {
display: inline-block;
margin-top: 12px;
font-size: 13px;
}
#status {
margin-top: 12px;
padding: 10px;
background: #f5f5f5;
border-radius: 6px;
font-size: 13px;
}
const box = document.querySelector('.box');
const status = document.getElementById('status');
// animate() はこの場で再生を始め、操作用の Animation オブジェクトを返す
const animation = box.animate(
[
{ transform: 'translateX(0)', backgroundColor: '#007bff' },
{ transform: 'translateX(200px)', backgroundColor: '#ff7043' },
],
{
duration: 1200,
easing: 'ease-in-out',
iterations: 4, // 4回くり返す
direction: 'alternate', // 往復させる
fill: 'forwards', // 終了後も最後の状態を保つ
}
);
document.getElementById('play').addEventListener('click', () => animation.play());
document.getElementById('pause').addEventListener('click', () => animation.pause());
document.getElementById('reverse').addEventListener('click', () => animation.reverse());
document.getElementById('finish').addEventListener('click', () => animation.finish());
document.getElementById('cancel').addEventListener('click', () => animation.cancel());
// playbackRate は再生速度。2 なら2倍速、0.5 ならスロー再生
document.getElementById('rate').addEventListener('change', (event) => {
animation.playbackRate = Number(event.target.value);
});
// 終わったときの処理は finished プロミスで書ける
animation.finished
.then(() => console.log('アニメーションが終わりました'))
.catch(() => console.log('キャンセルされました'));
// 状態を毎フレーム表示する
function render() {
const time = animation.currentTime === null
? '-'
: Math.round(animation.currentTime) + 'ms';
status.textContent =
'playState: ' + animation.playState +
' / currentTime: ' + time +
' / playbackRate: ' + animation.playbackRate;
requestAnimationFrame(render);
}
render();
「一時停止」で止めてから「逆再生」を押すと、その場から巻き戻しが始まります。再生速度のセレクトを切り替えれば、動いている最中でも速度が変わることが確認できます。似ているようで結果が違うのが finish() と cancel() で、finish() は最後の状態まで飛ばして終わるのに対し、cancel() はアニメーションそのものをなかったことにするので、要素は元の位置へ戻り playState は 'idle' になります。
この「途中で止める」「向きを変える」「速度を変える」という操作は、CSS アニメーションでは簡単には書けません。animation-play-state でクラスを付け外しすれば一時停止はできますが、任意の時点へ移動したり速度を動的に変えたりするのは現実的ではないからです。
終わったあとの処理は finished と finish イベントで書く
アニメーションが終わったら要素を消したい、次の処理へ進みたい、といった場面は頻繁にあります。Animation には終了を知らせる仕組みが用意されていて、finished プロパティは終了時に解決するプロミスを返します。await と組み合わせれば、アニメーションを手順のひとつとして自然に書けます。
async function fadeOutAndRemove(element) {
const animation = element.animate(
{ opacity: [1, 0] },
{ duration: 300, fill: 'forwards' }
);
await animation.finished; // 終わるまで待つ
element.remove(); // 消えきってから取り除く
}
プロミスなので、複数のアニメーションがすべて終わるのを Promise.all() で待つ、という書き方もそのまま通用します。なお、途中で cancel() を呼ぶと finished は解決ではなく AbortError という名前の DOMException で reject されます。取り消す可能性があるなら try / catch で受け止めておきましょう。
プロミスを使わずに、イベントとして受け取ることもできます。onfinish にハンドラーを代入するか、addEventListener('finish', ...) を使う方法です。終了するたびに毎回呼びたい処理はこちらが向いています。
const animation = box.animate(
{ transform: ['scale(1)', 'scale(1.2)', 'scale(1)'] },
{ duration: 400 }
);
// 終了イベントで受け取る
animation.onfinish = () => {
box.classList.remove('is-active');
};
// 取り消されたときは cancel イベントが発生する
animation.oncancel = () => {
console.log('中断されました');
};
ひとつ注意したいのは、iterations: Infinity で無限にくり返すアニメーションは自力では終了しないため、finished も finish イベントも待っているだけでは発生しないという点です。無限ループを止めたいときは、こちらから cancel() や pause() を呼ぶことになります。
実行時に決まる値を動かせるのが WAAPI の強み
@keyframes はスタイルシートに書く静的な定義なので、「何ピクセル動かすか」を書く時点で決めておく必要があります。CSS 変数を使えばある程度は動的にできますが、結局は JavaScript から値を書き込むことになります。element.animate() なら、キーフレームはただの JavaScript のオブジェクトなので、その場で計算した値をそのまま渡せます。
次のデモは、クリックされた座標へボールを移動させるものです。開始位置も移動先もクリックするまで分からない値なので、CSS だけでは表現できません。移動が終わると finished プロミスが解決して、到着した座標を表示します。移動の途中で別の場所をクリックすると、動いているアニメーションを cancel() して新しい移動に切り替えます。
<p class="lead">灰色のエリアをクリックすると、その位置までボールが移動します。移動先の座標はクリックするまで分からないので、CSS の <code>@keyframes</code> では書けません。</p>
<div class="area">
<div class="ball"></div>
</div>
<p id="log">クリックしてください。</p>
body {
font-family: sans-serif;
margin: 16px;
}
.lead {
font-size: 13px;
color: #444;
}
.area {
position: relative;
height: 200px;
border: 1px solid #ddd;
border-radius: 8px;
background: #f5f5f5;
cursor: crosshair;
overflow: hidden;
}
.ball {
position: absolute;
top: 0;
left: 0;
width: 28px;
height: 28px;
border-radius: 50%;
background: #007bff;
}
#log {
margin-top: 12px;
font-size: 13px;
}
const area = document.querySelector('.area');
const ball = document.querySelector('.ball');
const log = document.getElementById('log');
let x = 0; // いまボールがいる座標を覚えておく
let y = 0;
let running = null;
area.addEventListener('click', async (event) => {
const rect = area.getBoundingClientRect();
const nextX = event.clientX - rect.left - ball.offsetWidth / 2;
const nextY = event.clientY - rect.top - ball.offsetHeight / 2;
// 移動中に次のクリックが来たら、いまのアニメーションは打ち切る
if (running) {
running.cancel();
}
// 開始位置も終了位置も実行時に決まる値
const animation = ball.animate(
[
{ transform: 'translate(' + x + 'px, ' + y + 'px)' },
{ transform: 'translate(' + nextX + 'px, ' + nextY + 'px)' },
],
{ duration: 600, easing: 'ease-out', fill: 'forwards' }
);
running = animation;
x = nextX;
y = nextY;
log.textContent = '移動中…';
try {
await animation.finished; // 終わるまで待つ
log.textContent = '到着しました(' + Math.round(nextX) + ', ' + Math.round(nextY) + ')';
} catch (error) {
// cancel() されると finished は AbortError で reject する
console.log(error.name);
}
});
ポイントは、キーフレームの文字列を 'translate(' + x + 'px, ' + y + 'px)' のように組み立てているところです。要素の現在位置や画面サイズ、サーバーから取得した値など、実行してみないと分からない数値をそのままアニメーションに載せられます。あわせて、cancel() したときの finished の reject を try / catch で受け止めている点も確認してみてください。
CSS の @keyframes とどう使い分けるか
element.animate() があるからといって、CSS アニメーションが不要になるわけではありません。どちらにも得意な場面があります。
| 観点 | CSS の @keyframes + animation | element.animate() |
|---|---|---|
| 動かす値の決め方 | 書いた時点で決まる(CSS 変数で補う) | 実行時に計算した値をそのまま使える |
| 再生・停止の制御 | クラスの付け外しが中心で細かい操作は苦手 | play() / pause() / reverse() で直接操作できる |
| 任意の時点へ移動 | 手段がない | currentTime に代入するだけ |
| 終了の検知 | animationend イベント | finished プロミス、finish イベント |
| JavaScript が無効なとき | そのまま動く | 動かない |
| コードの置き場所 | 見た目の定義をスタイルシートに集約できる | ふるまいと一緒に JavaScript 側にまとまる |
ホバーで色が変わる、ページを開いたらふわっと出てくる、といった値が固定で制御も要らない演出は、CSS で書くほうが素直です。一方、移動量が実行時に決まる、途中で止めたり巻き戻したりしたい、終わったタイミングで次の処理につなげたい、という要件が出てきたら element.animate() の出番だと考えてください。
思ったとおりに動かないとき
書き方自体は難しくありませんが、CSS アニメーションの感覚のまま書くとつまずく場所がいくつかあります。
アニメーションが終わると見た目が元に戻る
移動させたはずの要素が、終わった瞬間に元の位置へ戻ってしまうケースです。原因は fill の既定値で、再生時間の外ではアニメーションの効果が適用されないためです。最後の状態を保ちたいときは fill: 'forwards' を指定します。
ただし fill: 'forwards' は、見た目を保つために終わったアニメーションを残し続ける指定でもあります。同じ要素に何度も適用すると、効果が積み重なっていきます。終わった状態を確定させたいなら、commitStyles() で結果をインラインスタイルとして要素に書き込み、アニメーション自体は cancel() で片づけるのが確実です。
const animation = box.animate(
{ transform: ['translateX(0)', 'translateX(200px)'] },
{ duration: 500, fill: 'forwards' }
);
animation.finished.then(() => {
animation.commitStyles(); // 最終状態を style 属性へ書き込む
animation.cancel(); // アニメーションは片づける
});
指定したプロパティが無視される
エラーは出ないのに、そのプロパティだけ変化しないときは書き方を疑ってください。よくあるのが background-color のように CSS そのままの名前で書いてしまうケースで、キーフレームでは backgroundColor と書く必要があります。もうひとつ多いのが単位の抜けで、{ width: 200 } のように数値だけを書くと有効な CSS の値になりません。{ width: '200px' } と文字列で指定します。
また、キーフレームで同じプロパティを重ねて書くことはできません。transform で移動と拡大を同時に行いたいときは、{ transform: 'translateX(100px) scale(1.2)' } のように1つの値としてまとめて書きます。
cancel() のあとに未処理のエラーが出る
await animation.finished で待っているアニメーションを cancel() すると、プロミスが AbortError で reject されます。受け止める先がないと「Uncaught (in promise) AbortError」といった未処理のエラーがコンソールに出ます。中断される可能性があるアニメーションでは、try / catch(または .catch())を必ず添えてください。
finish() で InvalidStateError になる
iterations: Infinity のアニメーションに対して finish() を呼ぶと、InvalidStateError が発生します。終わりのないアニメーションには「最後の状態」が存在しないためです。無限ループを止めるのは pause() か cancel() の役目だ、と切り分けておきましょう。
まとめ
element.animate(keyframes, options) は、CSS を書かずに JavaScript だけでアニメーションを作れるメソッドです。キーフレームは「時点ごとの状態を並べた配列」と「プロパティごとに値を並べたオブジェクト」の2通りで書け、options には duration / easing / delay / iterations / direction / fill といった、CSS の animation-* とほぼ同じ項目を指定できます。
そして最大の利点は、戻り値の Animation オブジェクトです。play() / pause() / reverse() / finish() / cancel() で再生を操作し、playbackRate や currentTime で速度や位置を変えられ、finished プロミスで終了を待てます。実行時に決まる値を動かしたいとき、JavaScript 側から止めたり巻き戻したりしたいときは、CSS アニメーションより element.animate() のほうが素直に書けます。用途で使い分けながら、動きのある UI を無理なく組み立ててみてください。