View Transitions API でカードをシュッと動かす
View Transitions API を使ってトランプゲームっぽく手札からカードを出して移動させるアニメーションを実装しました!
工夫した点や、詰まったところなどをまとめます。
全体的な実装と挙動は CodePen のサンプルを参照ください。
GIF アニメ:
View Transitions API とは
異なる DOM でのアニメーションを実装するための仕組みです。
概要は MDN のドキュメントや以下に紹介する記事がわかりやすいです。
実装の概要
トランジション実装のためにやることはシンプルです。
- 変更の前後で同じ要素として扱ってほしい要素に対して、CSS で
view-transition-name: <name>;
を指定する - JavaScript で
document.startViewTransition
を使って変更を発生させる
これだけです。
view-transition-name
の名前によって、異なる DOM でも同じ要素を特定でき、アニメーションが可能になるわけですね。
カードに一意な名前をつけるために UUID を使う
前述の通り、要素には一意な名前を設定する必要があります。
配列のインデックスを使ってしまうと、遷移の前後で名前が変わってしまうので、固有の ID を用意しておく必要があります。
crypto.randomUUID()
を使うことで、一意な ID を生成しました。
const uuid = () => crypto.randomUUID();
const cards = Array.from({ length: 4 }, () => ({ id: uuid() }));
cards.forEach((card, index) => {
const cardElement = document.createElement("button");
cardElement.style = `view-transition-name:card-${card.id}; contain: paint;`;
// ...
});
アニメーションの速度を変更する
::view-transition-group(*)
の animation-duration
を変更することでアニメーションの速度を変更できます。
::view-transition-group(*) {
animation-duration: 0.5s;
}
なお、 *
はすべてのビュートランジションに対する指定で、特定の名前を指定することもできます。
移動するカードを前面に出す
重なり順は DOM の重なり順で決まります。そのため、重なり順を制御したい場合は z-index
を使えば OK です。
今回は移動しているカードを動的に前面に出したいため、クリック時(遷移開始時)に z-index
を指定しています。
card.style.zIndex = "1";
void (async () => {
const viewTransition = document.startViewTransition(() => update());
await viewTransition.finished; // アニメーション終了を待つ
card.style.zIndex = ""; // 元に戻す
})();
デフォルトのクロスフェードをなくす
アニメーションを特に指定しない場合、デフォルトでクロスフェードが設定されています。このアニメーションが不要な場合、 ::view-transition-old
, ::view-transition-new
に設定されているアニメーションを無効化します。
::view-transition-old(*),
::view-transition-new(*) {
animation: none;
}
今回はカードを動かすときにフェードさせる必要はないと判断し、デフォルトのクロスフェードを打ち消しています。
ただし、すべて打ち消してしまうと、透過が効かなくなる(?)ため、移動するカードのみに適用するようにしています。
その場合、動的に style
要素を設定する必要があるため、JavaScript で設定します。
const styleElement = document.createElement("style");
// 移動するカードのクロスフェードを打ち消し
styleElement.textContent = `
::view-transition-old(card-${card.id}),
::view-transition-new(card-${card.id}) {
animation: none;
}
`;
document.head.appendChild(styleElement);
(async () => {
const viewTransition = document.startViewTransition(() => update());
await viewTransition.finished;
document.head.removeChild(styleElement);
})();
対応していないブラウザでのフォールバック
2024 年 6 月現在、Chrome, Edge, Opera で実装されていますが、Safari, Firefox では未実装です。
そのため、何も考えずに実装すると未対応ブラウザでエラーになります。
MDN などにも書いてありますが、実装されていない環境では View Transitions API を使わずに処理を行うようにすれば、アニメーションは行われませんが状態変化は行われます。
const update = () => {
// ...
};
if (document.startViewTransition) {
// ビュートランジション対応ブラウザ
document.startViewTransition(() => update());
} else {
// 非対応ブラウザ用のフォールバック
update();
}
トラブルシューティング
同じ名前が複数あるとエラーになる
当然ながら、遷移前・遷移後の状態で要素を 1 対 1 対応させる必要があるため、ページ内に同じ view-transition-name
が設定された要素が複数あるとエラーになります。
その場合はコンソールにエラーが出るので、うまくいかないときは確認しましょう。
TypeScript で書こうとすると型エラーが出る
document.startViewTransition
がまだ定義されていません。
@types/dom-view-transitions
をインストールすることで解決できます。
z-index が無視される?(重なり順が意図通りにならない場合)
position: absolute;
で配置した要素が、ビュートランジションでのアニメーション中に消えている(ように見える)ことがありました。
実際には消えているのではなく、z-index が意図通りになっておらず背後に隠れているだけでした。
View Transition における重なり順はなかなかややこしいです。
以下の記事でも説明されている通り、トランジションさせる必要がない要素にも view-transition-name
を設定する必要があります。
トランジション中の要素の重なり順の決定をまとめておくと、以下の順で判断されます。
-
view-transition-name
がある(トランジションする)要素が前、それ以外が後ろ- トランジションアニメーションを前面に乗っけているイメージ。
-
::view-transition-group(<name>)
に対するz-index
が大きいものが前、小さいものが後ろ - 元の要素の
z-index
が大きいものが前、小さいものが後ろ(普段通り)
つまり、 view-transition-name
が設定されていない要素は、絶対にトランジションしている要素よりも前に来ることができません。
そのため、常に前面に配置したい要素は、全く変化がないとしても view-transition-name
を設定する必要があります。
.description {
/* 変化がないとしても、他の要素の前に配置するために必要 */
view-transition-name: fixed-label;
}
おわりに
View Transitions API が出現する以前は、DOM が切り替わるときにアニメーションを実装しようと思うと、頑張って 2 つの要素を同じ位置に配置して重なっているように見せる…みたいな実装が必要でした。もしくは、アニメーションの実装を優先させて、セマンティクスを犠牲にすることもあったりしました。
View Transitions API によって HTML 構造を保ちつつ、お手軽にアニメーションを実装できるようになり感動です。 🥰
Discussion