小さく始めるView Transitions API
0.概要
2023年3月にリリースされたView Transitions API。トランジション時のリッチなアニメーションを簡単に実装できる手法として、たびたび話題になってます。
まだChromeしか対応していませんが、いざサポートが拡大した時に取り残されないよう、すこーしだけ導入してみました。
対象読者
- まだView Transitions APIを触ったことがない人
- 触ったことあるけど、実際に導入する手順がぱっと思い浮かばない人
※ 本記事のコードはReact
とTypeScript
を前提に書いております。
今回作ったリポジトリ
サイト
Next.jsのブログテンプレートを使って各所にView Transitions API
を適用してみました。
トップページの画像と記事詳細からトップに戻るボタンに適用済み、記事タイトルには適用してないので、ぜひ比べてみてください。
1.View Transitions APIとは?
View Transitions APIは、ページやUIの切り替え時にスムーズなトランジション効果を提供する新しいAPIです。これを使うことで、ユーザー体験を向上させることができます。具体的には、ページ遷移やコンテンツの変更時にフェードイン・フェードアウトなどのアニメーション効果を簡単に適用できます。このAPIは、従来の手動でのアニメーション実装よりも簡単で一貫性のある方法を提供します。
2.下準備
2-1. 型ファイルの作成
View Transitions APIはまだChromeしかサポートしていないため、TypeScriptの型に含まれていません。
サポートブラウザが2つ以上になると追加されるそうです。
↓参考
ちなみにライブラリが型ファイルを用意してくれてる場合もあるので、 どこかでdocument.startViewTransition
と書いてみて型エラーになるか確認してみましょう。型エラーにならなかったら本工程は不要です。
(2024/07/10時点だとReact Router
は用意してくれていますが、Next.js
は無かったです)
// global.d.ts
export interface ViewTransition {
ready: Promise<void>;
finished: Promise<void>;
updateCallbackDone: Promise<void>;
skipTransition: () => undefined;
}
declare global {
interface Document {
startViewTransition?: (cb: () => Promise<void> | void) => ViewTransition;
}
}
作成したファイルをtsconfig.json
のinclude
に追加します。
{
"compilerOptions": {
// 省略
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"src/types/global.d.ts" // ここ
],
"exclude": ["node_modules"]
}
2-2. startViewTransitionのラッパー関数を作成
今回はView Transitions APIの中核となるdocument.startViewTransition
を素朴に使ってきます。
ただし、このAPIはまだすべてのブラウザでサポートされているわけではないため、FirefoxやSafariといった未対応ブラウザでエラーにならないようにラッパー関数を作成します。
// viewTransitionAPIラッパー関数
export const startViewTransition = (callback) => {
if ('startViewTransition' in document) {
document.startViewTransition(callback);
} else {
// 未対応ブラウザのためのフォールバック処理
callback();
}
};
3. startViewTransitionを使ってみる
3-1.DOMを更新する関数に適用する
次に、DOMの変更があるところでstartViewTransition
関数を使います。例えば、React Routerを使ったhistory.push
などの操作に適用してみます。
import { useHistory } from '`React Router`-dom';
const MyComponent = () => {
const history = useHistory();
const handleNavigation = () => {
startTransition(() => {
history.push('/next-page');
});
};
return (
<button onClick={handleNavigation}>次のページへ</button>
);
};
3-2. Linkコンポーネントに適用する
onClick
などのイベントハンドラーに適用する場合は3-1
だけで十分ですが、せっかくなのでReact Router
やNext.jsでよく使うLinkコンポーネントにも適用してみます。
まずはRouterを拡張します。
// useViewTransitionRouter.ts
import { useRouter } from 'next/navigation';
import { startViewTransition } from './startViewTransition';
export const useViewTransitionRouter = () => {
const router = useRouter();
return {
...router,
push: (href: string) => {
startViewTransition(() => router.push(href));
},
};
};
次にLinkコンポーネントを拡張します。
// ViewTransitionLink.ts
import { useViewTransitionRouter } from '@/lib/useViewTransitionRouter';
import NextLink from 'next/link';
export const ViewTransitionLink = ({
href,
...props
}: React.ComponentProps<typeof NextLink>) => {
const router = useViewTransitionRouter();
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
router.push(href.toString());
};
return <NextLink href={href} {...props} onClick={handleClick} />;
};
任意の場所に適用してみます。
// cover-iamge.tsx
'use client';
import cn from 'classnames';
import Image from 'next/image';
import { ViewTransitionLink as Link } from './view-transition-link';
type Props = {
title: string;
src: string;
slug?: string;
};
const CoverImage = ({ title, src, slug }: Props) => {
const image = (
<Image
src={src}
alt={`Cover Image for ${title}`}
className={cn('shadow-sm w-full', {
'hover:shadow-lg transition-shadow duration-200': slug,
})}
width={1300}
height={630}
/>
);
return (
<div className='sm:mx-0'>
{slug ? (
<Link href={`/posts/${slug}`} aria-label={title}>
{image}
</Link>
) : (
image
)}
</div>
);
};
export default CoverImage;
これでLinkコンポーネントでもstartViewTransition
が適用されました。
ViewTransitionAPIのデフォルトのアニメーションはフェードイン・フェードアウトです。これにより、ページやUIの切り替えがスムーズになります。
導入はこれで終わり。晴れてView Transitions APIデビューです。
4. まとめ
最初は嬉しくなって色んなところにstartViewTransition
を入れたのですが、過剰だったり効果が薄かったりするケースも多く、結局導入は数か所にとどまりました。
アニメーションが苦手な方もいるので、prefers-reduced-motion
も適用しつつ、使いすぎには注意しましょう。
TimeTreeのエンジニアによる記事です。メンバーのインタビューはこちらで発信中! note.com/timetree_inc/m/m4735531db852
Discussion