🎬

React Router v6 と View Transitions API を使ってページ移動をアニメーションにする

2023/03/11に公開

概要

Chrome 111 で、View Transitions API が利用できるようになりました 🎉
ユースケースとして一般的な React Router のページ切り替えアニメーションを試したので紹介します。

記事のデモは codesandbox で公開しています。

ポイント

Transition を伴う Navigate を hooks に切り出す

document.startViewTransition が存在することを確認したあとに callback として navigate を渡します。 hooks として切り出しておくと便利です。
下記の例では、transition のアニメーションを指定できるように document に付与する className を string で渡せるようにしています。

import { useCallback } from "react";
import { useNavigate } from "react-router-dom";

declare global {
  interface Document {
    startViewTransition?: (callback: () => void) => any;
  }
}

export const useTransitionNavigate = () => {
  const navigate = useNavigate();

  const transitionNavigate = useCallback(
    async (
      newRoute: string,
      transitionClass: "slide-to-left" | "slide-to-right" = "slide-to-left"
    ) => {
      if (!document.startViewTransition) {
        return navigate(newRoute);
      }

      document.documentElement.classList.add(transitionClass);

      const transition = document.startViewTransition(() => {
        navigate(newRoute);
      });

      try {
        await transition.finished;
      } finally {
        document.documentElement.classList.remove(transitionClass);
      }

      return;
    },
    [navigate]
  );

  return {
    transitionNavigate
  };
};

固定したい要素などは view-transition-name を設定し view-transition-group を作成する

例えば Header の用に固定したい要素などは view-transition-name を設定すると良いです。
React Router でいうと Routes 内の <Outlet /> の要素だけ transitions したくなることが多そうです。

<header style={{ viewTransitionName: "header" }}>
 ~
</header>

animation は Global CSS で定義しておく

::view-transition-old::view-transition-new で animation の内容を記述します。
className を指定したり、 root の部分に view-transition-group の名前を指定も出来ます。

.slide-to-left::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
.slide-to-left::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
.slide-to-right::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}
.slide-to-right::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left;
}
@keyframes fade-in {
  from {
    opacity: 0;
  }
}
@keyframes fade-out {
  to {
    opacity: 0;
  }
}
@keyframes slide-from-right {
  from {
    transform: translateX(50%);
  }
}
@keyframes slide-to-left {
  to {
    transform: translateX(-50%);
  }
}
@keyframes slide-from-left {
  from {
    transform: translateX(-50%);
  }
}
@keyframes slide-to-right {
  to {
    transform: translateX(50%);
  }
}

まとめ

もっとスマートなやり方がこれから出てきそうですが、APIとしては非常にシンプルなので依存なくすぐに取り入れられますね。

アニメーションは邪魔にもなるので気をつけて使った方が良いですし、実際は下記のようなアニメーションをオフできるCSSも用意した方が好ましいです。

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

もっと深堀りしたい方は Jake さんの記事が詳しいです。本記事も参考に実装しています。
https://developer.chrome.com/docs/web-platform/view-transitions/

Discussion