コハム

Webクリエイターのコハムが提供する、Web制作に役立つ情報をまとめたブログです。

インパクト大!View Transition APIによるページ遷移の実装例

View Transition API

記事は上記記事を意訳したものです。

※当ブログでの翻訳記事は元サイト様に許可を得て掲載しています。


View Transition APIは他ページへの遷移にアニメーション効果を適用するCSSです。 2024年秋に予定されているiOS18に搭載されるSafariで使用可能となり、主要ブラウザすべてでサポートされることが見込まれます。 これまでにないダイナミックな効果を表現できるView Transition APIは使いこなせばWeb制作者にとって大きな武器となるでしょう。

基本的な設定

View Transition APIは、おそらく最も興奮する新しいCSSメカニズムの1つです。SPA(シングルページアプリケーション)のようなページ遷移をネイティブに構築する方法を提供することを約束しています。(SPAどころか、映画のワイプ、ビデオゲームのメニューを思い浮かべてみてください!フアンシーなDVDメニューを覚えていますか?)

最初に、View Transitionsは実験的な機能なので、Chrome Canaryで2つのフラグを有効にする必要があります。探しているフラグは、view-transitionとview-transition-on-navigationです。

それを私たちのページで実行するには、ナビゲートする元とナビゲート先の両方のドキュメントでメタタグを設定する必要があります。

<!-- 非推奨: -->
<meta content="same-origin" name="view-transition" />

仕様が更新されたため、Jake Archibalさんに新しい方法を教えていただきました。view transitionsをページで有効にするための新しい方法はこちらです。

/* ソースとターゲットのCSSで */
@view-transition {
    navigation: auto;
}

これは、以前のメタタグに置き換わるものです。

1つのページから別のページへナビゲートすると、サイト間でクロスフェードが表示されます。これがデフォルトの遷移です。カスタムの遷移も割り当てることができます。

::view-transition-old(root), ::view-transition-new(root) {
    animation: var(--my-fancy-animation-out);
}

また、選択された要素にアニメーションを割り当てることもできます。これが、要素がうまく滑り込むような、こだわりのあるトランジションにつながります。

::view-transition-old(swoop),
::view-transition-new(swoop) {
    animation: var(--my-fancy-animation);
}

.element {
    view-transition: swoop;
}

魔法のように、要素は位置と内容をモーフィングします。これは自動的に行われ、古い要素をフェードアウトし、新しい要素をフェードインします。消えていく要素と現れる要素の両方にカスタムアニメーションを設定することができます。

::view-transition-old(root) {
    /* 古い要素を何らかの形で消失させる */
    animation: var(--my-fancy-animation-out);
}

::view-transition-new(root) {
    /* 新しい要素を代わりに表示する */
    animation: var(--my-fancy-animation-in);
}

ファインチューニング

現在のところ、アニメーションは可能な限り常にトリガーされます。これには、望ましくない副作用がある可能性があります。類似した要素が移動元と移動先の両方を示すのは良いことですが、画面外から移動してくると、すぐに混乱してしまいます。

Intersection Observerを使用すると、要素が画面内にあるときのみview transitionを設定できます。

const observer = new IntersectionObserver(
    (entries) => {
        entries.forEach((entry) => {
            const el = entry.target as HTMLElement;
            const transitionName = el.dataset.transitionName;
            (el as HTMLElement).style.setProperty(
                "--transition-name",
                entry.isIntersecting ? `${transitionName}` : "none"
            );
        });
    }
);

const animatables = Array.from(document.querySelectorAll('.animatable'));
animatables.forEach((animatable) => {
    observer.observe(animatable);
});
.animatable {
    view-transition: var(--transition-name, none);
}

場合によっては、片方向の遷移でのみ再生したいこともあります。例えば、カスタムの遷移が設定されていない場合は、フルページのクロスフェードを行う必要はありません。ターゲットページでメタタグを削除すれば良いように思えますが、ターゲットページ自体が別の遷移のトリガーとなる必要がある場合は、この方法は使えません。私の解決策は、アンカー要素のクリックイベントリスナーを登録し、ブラウザがナビゲートする前にメタタグを削除して、選択したトリガー要素以外の遷移をキャンセルすることです。

const triggers = Array.from(document.querySelectorAll('[data-transition-trigger="true"]'));

(Array.from(document.querySelectorAll("a[href]")) as HTMLAnchorElement[]).forEach((el) => {
    if (triggers.includes(el)) {
        return;
    }

    el.addEventListener("click", () => {
        document.querySelector('meta[name="view-transition"]')?.remove();
    });
});

これで、data-transition-trigger="true"<a href=""> タグに設定することで、どの遷移がどの要素でスタートするかを細かく調整できます。

不具合

いくつかのことが奇妙だったり、バグっぽく感じられます。

上記のように、特定の遷移のトリガーとエンドポイントを作成するのが難しいです。アニメーションを片方向で再生する意味がある場合もありますが、view-transitionプロパティを設定するとトリガーとレシーバーの両方になってしまいます。

遷移中の要素が、場合によってはスタッキングコンテキストを無視するようです。絶対位置指定やstickyの要素が、z-indexを適用(そして強制)しても、遷移する要素の下に表示されてしまいます。

要素を拡大するのが適切に機能していません。transform: scale()で正の値を設定できますが、アニメーションが無効になり、新しい状態にスキップしてしまいます。

場合によっては、要素に設定されたプロパティを上書きできないこともあります。背景色の遷移を行うには不透明度を設定する必要がありますが、不透明度自体は上書きできません。ただし、visibilityは上書きできます。

これらのバグの多くは、Chromeの現在の実装におけるバグだと思われます。この機能は実験的なものですからね。

バットマン!!!!!

私が遷移を考えるとき、映画のようなシネマティックな遷移を想像します。「スターウォーズ」のスローなソフトワイプや、「イージーライダー」の奇妙な行ったり来たりなど、いくつかのアイコニックな遷移がありますが、おそらく60年代の「バットマン」のくるくる回転が最も楽しい遷移でしょう。

文書を回転させるのは、スピンアニメーションで簡単にできるはずです。

@keyframes spin {
    to {
        transform: rotate(2turn);
    }
}

しかし、まわりを回るバットマンのロゴをズームさせるのは? 要素を拡大することが適切に機能しないことを覚えていますか? それを回避するためのハックがあります。transform: scale()を使う代わりに、transform: translateZ()を使うのです。これは1pxに設定しておく必要がありますが、パースペクティブはフリーにアニメーション化できるので、Webで目まぐるしいショットを実現できます。アニメーション外では視覚プロパティを使って非表示にします。なぜなら、不透明度はバグがあるからです。要素の基本的な不透明度から、アニメーションの不透明度を差し引いてしまうため、完全に表示されなくなってしまうのです。

@keyframes scale-up {
    from {
        visibility: visible;
        transform: perspective(400px) translateZ(1px);
        background: url(./logo.png) center no-repeat;
        background-size: contain;
    }

    to {
        visibility: visible;
        transform: perspective(1px) translateZ(1px);
        background: url(./logo.png) center no-repeat;
        background-size: contain;
    }
}

::view-transition-old(batman) {
    animation: 0.5s linear both scale-up;
}

::view-transition-new(batman) {
    animation: 0.5s linear both scale-down;
    animation-delay: 0.5s;
}

#logo {
    position: absolute;
    top: calc(50% - 50px);
    left: calc(50% - 50px);
    width: 100px;
    height: 100px;
    visibility: hidden;
    view-transition-name: batman;
}

©コハム