第1回
<ViewTransition>でアニメーション遷移を実際に試す
Reactの実験的なViewTransitionコンポーネントを実際に試す。リストの並び替えや要素の出入りにアニメーションを付ける方法を、Vite環境で動かしながら解説する。
このシリーズ「React最新ラボ」では、最近のReact界隈の新しい技術を実際に手元で動かしながら検証していく。初回は、アニメーション系で話題の <ViewTransition> を試す。
このシリーズの前提:
useState/useEffectやコンポーネント分割といった基礎は分かっている中級者向け。React基礎が不安なら先に「React基礎 完全ガイド」を。
<ViewTransition> とは何か
ブラウザには View Transition API(document.startViewTransition)という仕組みがある。DOMが切り替わる「前」と「後」のスナップショットを撮り、その差分を自動でアニメーションしてくれるものだ。
ただし素のAPIはDOMを直接操作する前提で、Reactの「状態を変えたら画面が変わる」という宣言的な書き方と噛み合わせるのが面倒だった。<ViewTransition> は、Reactの状態更新に合わせて自動でView Transitionを発火してくれるラッパーになっている。
試す準備
<ViewTransition> はまだ安定版には入っていない。Canary / Experimental チャンネルでだけ使える。試すには実験版を入れる。
npm install react@experimental react-dom@experimental⚠️ 実験的API:本番では使わない前提。APIは予告なく変わる可能性がある。あくまで「今のうちに触って動きを掴む」ためのもの。
最小例:要素の出入りにアニメを付ける
まずは一番シンプルな「表示・非表示の切り替え」から。
import { ViewTransition, startTransition, useState } from 'react'
export function HelloToggle() {
const [show, setShow] = useState(false)
return (
<>
<button
onClick={() => {
// ★ 状態更新を startTransition で包むのがポイント
startTransition(() => setShow((v) => !v))
}}
>
切り替え
</button>
{show && (
<ViewTransition>
<p className="card">こんにちは!</p>
</ViewTransition>
)}
</>
)
}ボタンを押すと、<p> が単に出現するのではなく、ブラウザ標準のクロスフェードで現れる。
こんにちは!
アニメを自分で決める:enter / exit とCSS
ブラウザ標準のクロスフェードではなく、自分でアニメを決めたいときは、enter(追加時)・exit(削除時)にクラス名を渡し、CSSの ::view-transition-new / ::view-transition-old に書く。
<ViewTransition enter="slide-in" exit="slide-out">
<p className="card">こんにちは!</p>
</ViewTransition>@keyframes slideIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
/* enter="slide-in" → 新しく入ってくる側のスナップショット */
::view-transition-new(.slide-in) {
animation: slideIn 300ms ease;
}
/* exit="slide-out" → 消えていく側のスナップショット */
::view-transition-old(.slide-out) {
animation: slideIn 300ms ease reverse;
}enter / exit などのプロパティに渡せる値は3種類。
| 値 | 意味 |
|---|---|
"auto" |
ブラウザ標準のクロスフェード |
"none" |
アニメーションしない |
"クラス名" |
::view-transition-*(.クラス名) のCSSを当てる |
主なプロパティ
<ViewTransition> は「どのタイミングでアニメするか」をプロパティで指定する。
| prop | 動くタイミング |
|---|---|
enter |
この要素が新しく追加されたとき |
exit |
この要素が削除されたとき |
update |
中身が変わった・レイアウトがずれたとき |
share |
同じ name を持つ別の <ViewTransition> が出入りしたとき(共有要素遷移) |
default |
上記が未指定のときのフォールバック |
リストの並び替えを滑らかにする(共有要素遷移)
「同じ要素が場所を移動する」アニメは name を付けると実現できる。並び順が変わっても、Reactが同じ name の要素を「同一物」と見なし、位置の移動をアニメしてくれる。
import { ViewTransition, startTransition, useState } from 'react'
const initial = [
{ id: 'a', label: 'りんご' },
{ id: 'b', label: 'みかん' },
{ id: 'c', label: 'ぶどう' },
]
export function SortableList() {
const [items, setItems] = useState(initial)
const shuffle = () => {
startTransition(() => {
setItems((prev) => [...prev].reverse())
})
}
return (
<>
<button onClick={shuffle}>並び替え</button>
<ul>
{items.map((item) => (
// ★ name は同時に存在する中で一意にする
<ViewTransition key={item.id} name={`fruit-${item.id}`}>
<li>{item.label}</li>
</ViewTransition>
))}
</ul>
</>
)
}遷移の「向き」でアニメを変える:addTransitionType
「次へ」は左にスライド、「戻る」は右にスライド――のように、同じコンポーネントでも文脈でアニメを変えたいときは addTransitionType() を使う。
import { addTransitionType, startTransition } from 'react'
function goNext() {
startTransition(() => {
addTransitionType('nav-forward')
setPage((p) => p + 1)
})
}
function goBack() {
startTransition(() => {
addTransitionType('nav-back')
setPage((p) => p - 1)
})
}プロパティ側は、型ごとに値を分けたオブジェクトで受ける。
<ViewTransition
enter={{
'nav-forward': 'slide-left',
'nav-back': 'slide-right',
default: 'none',
}}
>
{/* ページ本体 */}
</ViewTransition>現場で使うときの注意
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}- 実験的:
react@experimentalが必要。本番では使わない。APIは変わりうる。 enter/exitはトップレベルで:<ViewTransition>の直前に別のDOMノードがあると、enter / exit が発火しない。<div><ViewTransition>…</div>のように包むと効かないことがある。- 対応ブラウザ:View Transition API は Chromium 系が先行。未対応ブラウザではアニメせず一瞬で切り替わるだけで、壊れはしない(段階的強化)。
📁 第1回の完成コード(並び替えデモ)
フォルダ構成
src/
├── components/
│ └── SortableList.tsx ← 今回作成
├── index.css ← ::view-transition-* のCSSを追記
└── App.tsxSortableList.tsx
import { ViewTransition, startTransition, useState } from 'react'
type Fruit = { id: string; label: string }
const initial: Fruit[] = [
{ id: 'a', label: 'りんご' },
{ id: 'b', label: 'みかん' },
{ id: 'c', label: 'ぶどう' },
]
export const SortableList = () => {
const [items, setItems] = useState<Fruit[]>(initial)
const shuffle = () => {
startTransition(() => {
setItems((prev) => [...prev].reverse())
})
}
return (
<div>
<button onClick={shuffle}>並び替え</button>
<ul>
{items.map((item) => (
<ViewTransition key={item.id} name={`fruit-${item.id}`}>
<li>{item.label}</li>
</ViewTransition>
))}
</ul>
</div>
)
}index.css(抜粋)
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}まとめ
<ViewTransition>は、ブラウザの View Transition API を Reactの状態更新に繋ぐ実験的コンポーネント- アニメの発火には
startTransition(またはuseTransition/<Suspense>/useDeferredValue)が必要。ただのsetStateでは動かない - 見た目は
::view-transition-old/::view-transition-newのCSSで、要素の移動はnameの共有要素遷移で - 実験的なので本番は避け、
prefers-reduced-motionへの配慮を忘れずに
次回は、最小のReactフレームワーク Waku を取り上げる。Server Components を一番小さく試して、このサイトと同じ Cloudflare にデプロイするところまでをやる。