Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Reactの<ViewTransition>で作るUIアニメーション

Avatar for teamLab teamLab PRO
December 03, 2025

 Reactの<ViewTransition>で作るUIアニメーション

【teamLab Study Session ~frontend~ #2 】にてチームラボのフロントエンドエンジニアが登壇用に作成したスライドです。

teamLab Study Session ~frontend~ とは

チームラボのフロントエンド班がチームラボ内で定期的に実施している勉強会を、フロントエンドの技術に興味ある参加者を募集し、オンラインで配信する勉強会です。

https://teamlab.connpass.com/event/373566/

Avatar for teamLab

teamLab PRO

December 03, 2025
Tweet

More Decks by teamLab

Other Decks in Programming

Transcript

  1. © teamLab Inc. Reactの<ViewTransition>で作る UIアニメーション teamLab Study Session ~frontend~ #2

    鹿島 匠 チームラボ パッケージチーム フロントエンド班
  2. © teamLab Inc. 所属| 2 自己紹介 2 パッケージチーム フロントエンド班 鹿島

    匠 ReactとTypeScriptのファン 手触りの良いアプリや便利なツールを作ることに関 心があります。 Kashima Takumi
  3. © teamLab Inc. ViewTransitionでカバーできるアニメーション - ページ遷移時のシームレス要素移動 - YouTubeのミニプレイヤー化など - ネイティブアプリのような遷移時スライド

    - ダイアログの消失時のフェードアウト - カルーセルのスライド - アコーディオン開閉時の伸長 ページ遷移だけでなく、要素単位の遷移にも使える! 7
  4. © teamLab Inc. ViewTransition以前の遷移アニメーション react-transition-groupや(Framer) Motionなどで実現されてきた 1. 古いコンテンツを残しつつ、 新しいコンテンツをマウント 2.

    "hoge-enter"や"hoge-exit" みたいなクラス名を付与 3. いい感じに位置やz-indexを調整しつつ、 CSSでアニメーション 4. アニメーションが終わったら、 古いコンテンツをアンマウント 大変! 9 old new old [hoge-exit] new [hoge-enter] old [hoge-exit] new [hoge-enter]
  5. © teamLab Inc. 1. document.startViewTransition()などによりViewTransitionが発生 2. view-transition-name styleがついている要素の スナップショットが取得される(old) 3.

    startViewTransitionに渡したcallbackが実行され、 DOM変更が反映される 4. view-transition-name styleがついている要素の スナップショットが取得される(new) 5. 擬似要素でold→newのアニメーションが表示 ViewTransitionのプロセス 13 old new ::view-transition
  6. © teamLab Inc. startViewTransitionとReactの相性 17 実はstartViewTransition()はReactと相性がよくない const [page, setPage] =

    useState(0); const handleClickRight = () => { document.startViewTransition(() => { setPage(prev => prev + 1); // 同期的にDOMに反映されない! }); } React は state の更新をまとめて行います(バッチ処理) 。すべてのイベントハンドラを実行し終え 、set 関数が呼び出され た後に、画面を更新します。 
 https://ja.react.dev/reference/react/useState#setstate-caveats 

  7. © teamLab Inc. startViewTransitionとReactの相性 18 実はstartViewTransition()はReactと相性がよくない const [page, setPage] =

    useState(0); const handleClickRight = () => { document.startViewTransition(() => { flushSync(() => { setPage(prev => prev + 1); // 動かなくはないが... }) }); } Reactのスケジューラの恩恵を受けられない 再レンダリングが重かったりすると、スレッドがブロッキングされちゃうので、画面が固まったりする
  8. © teamLab Inc. startViewTransitionとReactの相性 19 実はstartViewTransition()はReactと相性がよくない const router = useRouter();

    const handleClickRight = () => { document.startViewTransition(() => { flushSync(() => { router.push("/home"); // 🤯 }) }); } const { data, refetch } = useQuery(); const handleClickSync = () => { document.startViewTransition(async () => { await refetch(); // 🫨 }); } 命令的なAPIである点と、Reactのスケジューリング機構との噛み合わなさ
  9. © teamLab Inc. <ViewTransition> Reactのトランジション概念と紐付けられている 21 const [page, setPage] =

    useState(0); const handleClickRight = () => { startTransition(() => { // トランジションのアクションによって起こるDOM反映でViewTransitionが発火するようになる! setPage(prev => prev + 1); }) } return ( <ViewTransition> <Page pageIndex={page} /> </ViewTransition> )
  10. © teamLab Inc. 下準備 24 <ViewTransition> コンポーネント機能を使う にはcanary版Reactが必要 Next.js 15の場合はnext.config

    のexprerimental.viewTransition をtrueに https://nextjs.org/docs/app/api-referen ce/config/next-config-js/viewTransition "dependencies": { "@react-router/node": "^7.9.2", "@react-router/serve": "^7.9.2", "isbot": "^5.1.31", "react": "19.3.0-canary-fb2177c1-20251114", "react-dom": "19.3.0-canary-fb2177c1-20251114", "react-router": "^7.9.2" }, "devDependencies": { "@biomejs/biome": "^2.3.6", "@react-router/dev": "^7.9.2", "@types/node": "^22", "@types/react": "^19.2.6", "@types/react-dom": "^19.2.3", "typescript": "^5.9.2", "vite": "^7.1.7" }
  11. © teamLab Inc. 下準備 25 @typesは通常リリース版でOK だが、canary版の型定義を 読み込む必要がある tsconfigのtypesに"react/canary" を指定することで、型がcanary

    対応に切り替わる あるいはTriple-Slash Directiveで も /// <reference types="react/canary" /> // tsconfig.json "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2024"], "types": ["node", "vite/client", "react/canary"], "target": "ES2024", "module": "ES2024", "moduleResolution": "bundler", "jsx": "react-jsx", // ... }
  12. © teamLab Inc. 元のコード抜粋 26 ありきたり handはuseStateからの値 {hand.map((card, index) =>

    ( <div key={card.id} className={styles.cardWrapper} style={ { "--card-index": index, "--total-cards": hand.length, } as React.CSSProperties } > <PlayingCard card={card} /> </div> ))}
  13. © teamLab Inc. ①カードがずれるところにアニメーションを追加 27 ViewがTransitionしてほしい 要素を<ViewTransition> で囲う view-transition-nameはReactが ユニークな値をつけてくれる

    {hand.map((card, index) => ( <ViewTransition key={card.id}> <div className={styles.cardWrapper} style={ { "--card-index": index, "--total-cards": hand.length, } as React.CSSProperties } > <PlayingCard card={card} /> </div> </ViewTransition> ))}
  14. © teamLab Inc. ①カードがずれるところにアニメーションを追加 28 setHandしている部分を startTransition で囲う → state更新が

    トランジションとして マークされる startTransition(() => { setHand([...hand, drawnCard]); setDeck(remainingDeck); });
  15. © teamLab Inc. ①カードがずれるところにアニメーションを追加 29 - カードを引く時 - 既存カードがずれる時 にアニメーションが追加された

    このアニメーションは ブラウザ側既定のスタイル https://github.com/elecdeer/view-t ransition-trump/compare/79b74c3b5 373ba03a2c6aad3bee95283a1d41872... 8aa90ff62eb8e0e34862d7ce4c4c8f4d1 6ea1fef
  16. © teamLab Inc. share 要素が削除され、別の場所に同名で 挿入されたとき(移動) ReactのViewTransition 種別 30 enter

    要素が挿入されたとき exit 要素が削除されたとき update 要素が更新されたとき (DOMプロパティの変更など)
  17. © teamLab Inc. ②引く時と捨てる時にそれぞれアニメーションをつける 31 enterとexit propにCSSの識別 クラス名を指定する Reactがview-transition-class styleをDOM反映時に付けてく

    れる onEnterなどのイベントもある 擬似要素のDOMが取れるの で、.animate()で命令的に アニメーションもできる <ViewTransition key={card.id} enter={styles["cardEnter"]} exit={styles["cardExit"]} onEnter={(instance) => { console.log("onEnter", instance); }} onExit={(instance) => { console.log("onExit", instance); }} >
  18. © teamLab Inc. ②引く時と捨てる時にそれぞれアニメーションをつける 32 スナップショット(old)の擬似要素は ::view-transition-old(.cardExit) スナップショット(new)の擬似要素は ::view-transition-new(.cardEnter) 普通のCSSと同じよう

    keyframeを定義し、animationで 割り当てればOK ChromeだとcardSlideUpAndFadeOutの アニメーションが終わったあとに一瞬 animationなしの状態で表示され、 チラつきが発生する? Safariだと起きない(謎) ::view-transition-group(.cardEnter) { animation-duration: 0.3s; animation-timing-function: ease-out; } ::view-transition-old(.cardEnter) { animation: none; } ::view-transition-new(.cardEnter) { animation: cardSlideInFromRight 0.3s ease-out; } ::view-transition-group(.cardExit) { animation-duration: 0.3s; animation-timing-function: ease-in; } ::view-transition-old(.cardExit) { animation: cardSlideUpAndFadeOut 0.3s ease-in; /* Chromeだとアニメーション完了後に一瞬oldが表示されている? */ animation-fill-mode: forwards; } ::view-transition-new(.cardExit) { animation: none; }
  19. © teamLab Inc. ②引く時と捨てる時にそれぞれアニメーションをつける 33 - 引く時(挿入) - 捨てる時(削除) にそれぞれアニメーションが

    追加された https://github.com/elecdeer/view-t ransition-trump/compare/8aa90ff62 eb8e0e34862d7ce4c4c8f4d16ea1fef... ea3520f3f9d1e083ae3ce1d8382b05fa9 c3dcb74
  20. © teamLab Inc. ③別の場所に移動するときにアニメーションする 34 "同じ"要素にname propを 指定するとview-transition-name を指定できる 指定していない場合は一意なidが

    それぞれに割り振られる(これまで) 同時に同nameでマウントされてはいけない、 エラーになる あるnameをもつ要素が削除され、 別の場所に挿入されたと き、"share"でView Transitionが走 る <h2 className={styles.areaTitle}>場</h2> <div className={styles.field}> {field.map((card) => ( <ViewTransition name={`card-${card.id}`} key={card.id}> <div className={styles.fieldCard}> <PlayingCard card={card} /> </div> </ViewTransition> ))} </div> // 中略 .... <h2 className={styles.areaTitle}>手札</h2> <div className={styles.linearLayout}> {hand.map((card, index) => ( <ViewTransition name={`card-${card.id}`} key={card.id} enter={styles["cardEnter"]} exit={styles["cardExit"]} > <div className={styles.cardWrapper} // ... > <PlayingCard card={card} /> </div> </ViewTransition> ))} </div>
  21. © teamLab Inc. ④アクションによってアニメーションを分ける 36 addTransitionTypeで トランジションに対して任意の typeをマークできる 画面遷移時のアニメーションを 遷移先によって分けたい場合に

    便利 const playCard = () => { if (selectedCards.length === 0) return; const cardsToPlay = hand.filter((c) => selectedCards.includes(c.id)); startTransition(() => { const ranksSet = new Set(cardsToPlay.map((c) => c.rank)); // 同じランクのカードを複数枚出す場合 if (selectedCards.length > 1 && ranksSet.size == 1) { addTransitionType("hand-of-same-rank"); } setHand(hand.filter((c) => !selectedCards.includes(c.id))); setField([...field, ...cardsToPlay]); setSelectedCards([]); }); };
  22. © teamLab Inc. ④アクションによってアニメーションを分ける 37 enter, share, exit, update, default

    propは オブジェクトも受け入れる addTransitionTypeで指定する type名をキーにできる defaultは特殊なキーで、該当 するtypeがない場合のfallback <ViewTransition name={`card-${card.id}`} key={card.id} enter={styles["cardEnter"]} exit={styles["cardExit"]} share={{ "hand-of-same-rank": `${styles["cardMove"]} ${styles["handOfSameRank"]}`, default: styles["cardMove"], }} > <div className={styles.fieldCard}> <PlayingCard card={card} /> </div> </ViewTransition>
  23. © teamLab Inc. Viewport内外を移動する要素に対してはshare ViewTransitionは発火しない 40 Reactの<ViewTransition>の仕様 マウントされる側、アンマウントされる側のどちらかが Viewportの外側にある場合、shareではなくenter /

    exit片方になる 遷移時に画面全体を動かす場合に引っかかったりするので注意 If either the mounted or unmounted side of a pair is outside the viewport, then no pair is formed. This ensures that it doesn’t fly in or out of the viewport when something is scrolled. Instead it’s treated as a regular enter/exit by itself. 
 https://ja.react.dev/reference/react/ViewTransition#animating-a-shared-element 

  24. © teamLab Inc. ViewTransitionに対応していないブラウザでは<ViewTransition>は無視される 非対応ブラウザのために、<ViewTransition>を使うかどうかを ユーザコード側で判定する必要は無い - document.startViewTransition === undefined

    とかは不要 Reactが判定してくれている https://github.com/facebook/react/blob/16e16ec6ffe159ba831203eeeb7efe72df82c4be/packages/react-do m-bindings/src/client/ReactFiberConfigDOM.js#L2331-L2337 41
  25. © teamLab Inc. SPAの画面遷移時にViewTransitionするには Reactの<ViewTransition>で画面遷移時にアニメーションするには、 routerがSuspenseに対応している必要がある - Next.js App router

    - React Router v7 は対応している(多分) Router側でReactとは別にViewTransitionの仕組みがある場合もあるので、 混同しないよう注意 実装上は遷移前と後の画面に、同じname propを持つ要素を置き、 必要に応じてaddTransitionTypeなどを使えば良いだけ 42
  26. © teamLab Inc. useQueryからの値の変更に対してViewTransitionするには useDefferedValueが使える🎉 44 const { data }

    = useQuery({ queryKey: ['someList'], queryFn: () => fetchSomeList(), refetchInterval: 5000, }); const deferredData = useDeferredValue(data); return ( <div> {deferredData.items.map(item => ( //... ))} </div> )
  27. © teamLab Inc. アクセシビリティの考慮 prefers-reduced-motion アニメーションを好まないユーザの為に、 @media (prefers-reduced-motion: reduce) クエリかアプリケーションの設定での

    アニメーションの無効化を検討する Visual Regression Testingの安定化にも役立つ 45 @media (prefers-reduced-motion: reduce) { ::view-transition-group(.cardEnter) { animation: none; } }
  28. © teamLab Inc. まとめ - View Transition APIは、 DOM変更時のシームレスな遷移アニメーションを実現するWeb API

    - ページ遷移だけでなく、要素単位の遷移にも使える - ページ遷移時のシームレス要素移動、ネイティブアプリのような遷移時アニメ - ダイアログの出現時 / 消失時のフェード、アコーディオン開閉時の伸長 - Reactのトランジションと統合された<ViewTransition>コンポーネントにより、 ViewTransitionをReactアプリケーションに簡単に取り込める - 遷移はenter / exit / update / shareの4種類があり、それぞれにアニメを割当てられる - addTransitinoTypeにより、アクションに応じたアニメの割り当てもできる - Reactのトランジションへの理解や、<ViewTransition>特有の仕様に注意 46