React

SSGOIとは?Next.jsでページ遷移アニメーションを付ける手順と7.5の変更点

SSGOIとは?Next.jsでページ遷移アニメーションを付ける手順と7.5の変更点

SSGOIは、Webアプリのページ遷移にネイティブアプリのようなアニメーションを付けるオープンソースのライブラリです。React・Svelte・Vue・Solid・Angular・Qwik向けのパッケージがあり、ライセンスはMITです。2026年11月8日時点の最新版は、11月6日公開の7.5.0です。

7.5.0でルーターのつなぎ方が変わり、これまで標準だった @ssgoi/react/nextjs の <SsgoiRouteBoundary> は非推奨になりました。8.0.0で削除される予定です。7.0.0(2026年7月26日)より前の記事に載っている <SsgoiTransition id> や defaultTransition のコードはさらに古く、そのままでは型エラーになるか、意図した遷移になりません。この記事では、7.5.0を前提にNext.js App Routerへの導入手順、遷移ルール、レイアウトの保持、旧版からの移行点を説明します。

まとめ:SSGOIの要点

  • SSGOIはルーターを置き換えず、フレームワークがページを付け外しするタイミングを監視して、出ていくページと入ってくるページをWeb Animations APIで動かす。
  • 7.5.0のNext.js導入は、<Ssgoi framework={...}> にルートの読み方を渡し、@ssgoi/react の <SsgoiLayoutBoundary> を置く形になった。外側の要素には position: relative; z-index: 0; overflow-x: clip が要る。
  • どの遷移にどの効果を当てるかは、on・from/to・ordered の3種類のルールで書く。ブラウザの戻る操作では、記録した効果が逆向きに再生される。
  • ヘッダーやタブを残すなら config.layouts、Next.jsのCache Componentsを使うなら <SsgoiPageBoundary> を使う。
  • プリセットは公式カタログに12種類ある。一覧から詳細へは drill、タブは slide、画像から詳細へは zoom が基本になる。
  • OSの「視差効果を減らす」設定は7.5.0でも自動では反映されない。prefers-reduced-motion を読んでルールを空にする処理を自分で書く。

SSGOIの仕組みと対応フレームワーク

ルート境界の付け外しを監視する仕組み

SSGOIは、URLの変更やページの読み込みには関与しません。ページの切り替えはNext.jsやReact Routerなど既存のルーターが担い、SSGOIは data-ssgoi-transition 属性の付いた要素(ルート境界)がDOMから外れる瞬間と、新しい境界が入る瞬間を MutationObserver で捉えます。

Reactが古いページを取り除くと、SSGOIはその要素を一時的に position: absolute で戻し、新しいページと重ねて動かします。アニメーション本体は、ばねの動きを事前に計算したキーフレームを Element.animate() に渡して再生します。公式ドキュメントによると、再生が始まった後はブラウザがネイティブに描画し、フレームごとのJavaScriptは走りません。

Next.js 16.4.5・React 19.3.1・@ssgoi/react 7.5.0の最小構成をビルドし、ヘッドレスのChrome 156(幅390×高さ844)で / から /posts/1 へ移動して確かめました(2026年11月8日・3回計測)。クリックから0.06〜0.13秒で document.getAnimations() に2本のアニメーションが現れ、0.7〜0.8秒前後まで再生されました。その間、旧ページの境界は position: absolute、新ページは relative でした。

対応パッケージとルーター

パッケージはすべて同じ版番号で同時に公開され、7.5.0の時点でnpmに出ているのは次の8つです。組み合わせる版は揃えます。

パッケージ 対象 接続できる主なルーター
@ssgoi/react React Next.js/Remix/React Router/TanStack Router
@ssgoi/svelte Svelte SvelteKit
@ssgoi/vue Vue Nuxt/Vue Router
@ssgoi/solid Solid Solid Router/SolidStart
@ssgoi/angular Angular Angular Router
@ssgoi/qwik Qwik Qwik City
@ssgoi/react-native React Native Expo Router
@ssgoi/core 共通エンジン なし

7.5.0のルーター接続は、どのフレームワークでもプロバイダーの framework にルートの読み方を渡す形にそろいました。実験的APIの扱いでないのはNext.jsだけで、SvelteKit・Nuxt・Vue Router・React Router・SolidStartの導入ガイドには Experimental API と書かれています。ルーター連携の安定性で選ぶなら、Next.jsから検証を始めるのが順当です。

ブラウザは、デスクトップとモバイルの現行Chrome・Safari・Firefox・Edgeが対象です(公式のBrowser & router support)。READMEはWeb Animations APIの動作目安としてChrome 84以降・Safari 13.1以降・Firefox 75以降・Edge 84以降を挙げています。sheet と zoom のぼかし(blur)は backdrop-filter を使うため、Firefox 102以前ではぼかしだけが省かれます。同じビルドはCapacitorやTauriなどのWebViewとPWAでも動きますが、iOSとAndroidのスワイプバックはOS側の動作に任せる設計です。

Next.js App Routerへの導入手順

Next.jsの基本的な構成はNext.js 16系App Routerの最小アプリを動かす手順で説明しています。ここでは既存のApp Routerプロジェクトに追加する前提で、触るファイルは2つです。公式の対象はApp Router 13.4〜16で、Pages Routerでは手動で境界を書く別の手順になります。

npm install @ssgoi/[email protected]

まず、設定と framework をクライアントコンポーネントに書きます。次の例では、/posts 配下の詳細ページへ入るときに drill を当てます。config と framework は必ずモジュールの最上位に置きます。描画のたびに新しいオブジェクトを作るとエンジンが作り直され、保存したスクロール位置が失われます。

// app/ssgoi-provider.tsx
"use client";

import type { ReactNode } from "react";
import { usePathname, useSelectedLayoutSegments } from "next/navigation";
import { Ssgoi, type SsgoiConfig, type SsgoiFramework } from "@ssgoi/react";
import { drill, fade } from "@ssgoi/react/view-transitions";

const config: SsgoiConfig = {
  transitions: [
    { priority: -100, on: "/**", transition: fade() },
    { on: "/posts/**", except: "/posts", transition: drill() },
  ],
};

const INTERCEPTION_PREFIX = /^(?:\(\.\.\.\)|\(\.\.\)|\(\.\))+/;

const framework: SsgoiFramework = {
  useRoute: () => {
    const pathname = usePathname();
    const segments = useSelectedLayoutSegments();
    if (pathname == null) return null;
    const renderedPath = `/${segments
      .filter((s) => !(s.startsWith("(") && s.endsWith(")")))
      .map((s) => s.replace(INTERCEPTION_PREFIX, ""))
      .filter(Boolean)
      .join("/")}`;
    return { pathname, renderedPath };
  },
};

export function SsgoiProvider({ children }: { children: ReactNode }) {
  return (
    <Ssgoi config={config} framework={framework}>
      {children}
    </Ssgoi>
  );
}

次に、ルートレイアウトでプロバイダーとルート境界を置きます。

// app/layout.tsx
import type { ReactNode } from "react";
import { SsgoiLayoutBoundary } from "@ssgoi/react";
import { SsgoiProvider } from "./ssgoi-provider";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <main
          style={{
            position: "relative",
            zIndex: 0,
            minHeight: "100dvh",
            overflowX: "clip",
          }}
        >
          <SsgoiProvider>
            <SsgoiLayoutBoundary>{children}</SsgoiLayoutBoundary>
          </SsgoiProvider>
        </main>
      </body>
    </html>
  );
}

renderedPath は、モーダルをソフトに割り込ませたときに「URLはモーダルでも背景のページは変わっていない」ことをSSGOIへ伝える値です。@modal のようなスロットは、境界の外に描画します。最初の <SsgoiLayoutBoundary> は <Ssgoi> と同じレイアウトに置きます。framework を渡し忘れると、境界は SSGOI: で始まるエラーを投げます。

main のスタイルにも役割があります。relative は一時的に戻された旧ページの配置の基準になり、z-index: 0 は重なり順の文脈を作ります。overflow-x を hidden にするとこの要素がスクロールコンテナになり、スクロール位置の復元が別の要素に向いてしまうので、clip にします。

遷移ルールの書き方:on・from/to・ordered

transitions には、効果そのものではなく「どのページ間の移動にどの効果を当て、どちらを前進とみなすか」を書きます。ルールは3種類です。

ルール 向いている画面 前進の判定 既定のスクロール
on+except 一覧と配下の詳細 範囲に入ると前進 戻り先は復元・進み先は先頭
from/to 特定の2画面の組 書いた向きが前進 戻り先は復元・進み先は先頭
ordered タブ・手順 配列の後ろへ行くと前進 両側とも復元

パスの指定は、完全一致と2種類のワイルドカードが基本です。/posts はその1ページ、/posts/* は1階層下のページ、/posts/** は /posts 自身と配下すべてに当たります。複数のルールに当たる場合は、priority の大きいもの、より具体的なパス、書いた順の順で優先されます。前の例で fade に priority: -100 を付けたのは、ほかのルールに当たらない移動の受け皿にするためです。

import type { SsgoiConfig } from "@ssgoi/react";
import { drill, fade, slide, zoom } from "@ssgoi/react/view-transitions";

const config: SsgoiConfig = {
  transitions: ({ isMobile }) => [
    { priority: -100, on: "/**", transition: fade() },
    { on: "/posts/**", except: "/posts", transition: drill() },
    { from: "/gallery", to: "/photo/*", transition: zoom({ type: "expand" }) },
    {
      ordered: ["/tabs/home", "/tabs/search", "/tabs/profile"],
      transition: isMobile ? slide() : fade(),
    },
  ],
};

isMobile はスクロールコンテナの幅が768px未満のときにtrueになります。コンテナの幅を取得できない場合はウィンドウ幅を使い、端末の種類そのものは判定しません。from/to のルールは既定で双方向に効き、片道だけにしたいときは bidirectional: false を付けます。

SSGOIは直近100件の履歴について効果と向きを記録します。ブラウザの「戻る」では記録した効果を逆向きに、「進む」では元の向きで再生します。リロードした後や、記録の無い履歴へ一気に飛んだ場合は、その時点のルールで判定し直します。遷移中のホイールやタッチによるスクロールは既定で抑止され、アプリ側でスクロールを固定しているなら設定の最上位に scrollLock: false を書きます。

レイアウトの保持と遷移状態の読み取り

config.layoutsによるヘッダー・タブの保持

境界が1つだけだと、ページ全体が入れ替わります。ヘッダーやタブを残して中身だけ動かすには、config.layouts に残したい範囲を書き、そのレイアウトの内側に2つ目の境界を置きます。深さは境界の入れ子の数で決まります。

// app/ssgoi-provider.tsx の既存のimportを次に変更
import { drill, fade, slide } from "@ssgoi/react/view-transitions"; // slide を追加

// 既存のconfigを置換
const config: SsgoiConfig = {
  layouts: [{ paths: "/shop/**" }],
  transitions: [
    { priority: -100, on: "/**", transition: fade() },
    { on: "/posts/**", except: "/posts", transition: drill() },
    { ordered: ["/shop/new", "/shop/sale"], transition: slide() },
  ],
};

// app/shop/layout.tsx
import type { ReactNode } from "react";
import { SsgoiLayoutBoundary } from "@ssgoi/react";

export default function ShopLayout({ children }: { children: ReactNode }) {
  return (
    <div>
      <header>カテゴリのタブ</header>
      <SsgoiLayoutBoundary>{children}</SsgoiLayoutBoundary>
    </div>
  );
}

この構成で /shop/new から /shop/sale へ移動すると、header のDOMノードは同じものが残り、アニメーションの対象は内側の境界2つだけでした。トップから /shop/new へ入るときは、ヘッダーごと動きます。範囲は /shop/** と書きます。/shop/* にすると、カテゴリごとに別のレイアウトとして扱われます。

Cache Components向けのSsgoiPageBoundary配置

next.config で cacheComponents: true を指定し、訪問済みのルートを <Activity> で生かしておく場合は、key の付け替えで作り直す <SsgoiLayoutBoundary> では状態が失われます。7.5.0で追加された <SsgoiPageBoundary> は key を変えず、ルート自身の表示・非表示を遷移として扱います。

公式ガイドの配置は、ルートレイアウトに <Ssgoi> だけを置き、ルートグループのレイアウトと各ページを <SsgoiPageBoundary> で包む形です。同じ領域で2種類の境界を混ぜてはいけません。ルートのパラメーターは useParams() や usePathname() ではなくページの params から読みます。隠れているルートも現在のURLで再描画されるためです。リリースノートには、Next.js側の並べ替えで内側のスクロール位置やiframeが初期化される問題が残っているとも書かれています。Cache Componentsを本番で有効にしていないなら、通常の <SsgoiLayoutBoundary> で十分です。

useSsgoiTransitionによる遷移完了の検知

アンカーへのジャンプや scrollIntoView、フォーカス移動を遷移の途中で実行すると、アニメーションと衝突します。7.5.0で追加された useSsgoiTransition() は、遷移の状態を status(idle か transitioning)で返すので、idle に戻ってから処理します。

"use client";

import { useEffect, useRef } from "react";
import { useSsgoiTransition } from "@ssgoi/react";

export function FocusHeading({ children }: { children: string }) {
  const ref = useRef<HTMLHeadingElement>(null);
  const { status } = useSsgoiTransition();

  useEffect(() => {
    if (status === "idle") ref.current?.focus();
  }, [status]);

  return <h1 ref={ref} tabIndex={-1}>{children}</h1>;
}

Chrome 156で計測すると、status は idle から transitioning に変わり、0.72〜0.82秒後に idle へ戻りました。フォーカスは最後のアニメーションの約0.05〜0.07秒後に移りました。どのルールにも当たらない移動では、status は idle のままです。

プリセット12種の選び方

効果は @ssgoi/react/view-transitions(Svelteなら @ssgoi/svelte/view-transitions)から読み込みます。公式カタログは、ユーザーに伝えたい操作の意味から効果を選ぶよう勧めています。

プリセット 使う場面 主な指定 想定する端末
drill 一覧から詳細、親から子 type: parallax(既定)/slide モバイル
sheet 投稿作成・絞り込みなど一時的な画面 type: static(既定)/scale/blur モバイル・Web
slide タブ・横方向の手順 なし モバイル
axis 兄弟ページ(Materialの共有軸) type: x(既定)/y/z モバイル
zoom カードや画像から詳細 type: static(既定)/expand/blur モバイル
scroll 縦に並ぶ全画面ページ type: directional(既定)など Web
hero 同じ画像を別ページへ引き継ぐ type: static(既定)/fade モバイル・Web
fade 関係の薄いページ間 なし Web
film・strip・rotate・jaemin 演出を強めたい画面 filmは枠色を指定可 Web

迷ったら、一覧と詳細の行き来に drill、受け皿に fade の2つで始め、画像の多い画面だけ zoom か hero を足します。jaemin や rotate は1画面の演出には向きますが、業務アプリの頻繁な移動に当てると操作のたびに待たされる感覚が残ります。

hero と zoom に必要なマークアップ

hero と zoom は、移動元と移動先で同じキーを付けた要素を対応させます。zoom の場合、移動先には enter のキーをちょうど1つだけ置きます。キーが空か一致しない場合や、移動先の enter のキーが0個または2個以上の場合、遷移は何もしない扱いになります。

// 一覧側(移動元)
<img data-zoom-exit-key={photo.id} src={photo.thumbnail} alt="" />

// 詳細側(移動先)
<img data-zoom-enter-key={photo.id} src={photo.full} alt="" />

hero の場合は data-hero-exit-key と data-hero-enter-key を使い、1回の遷移で複数の組を動かせます。写真アプリのように画像だけを目で追わせたいなら hero、カードがそのまま詳細画面に開く印象にしたいなら zoom です。7.5.0のリリースノートによると、hero と zoom はページ境界より後に現れる共有要素を最大500ms待つようになり、Suspenseで本文の表示が遅くても遷移が途切れにくくなりました。

ばねの硬さを変える override

各プリセットの第2引数に override を渡すと、前進と後退で動きを変えられます。ばねの設定は smooth・snappy・bouncy・gentle・swift・accelerate の6つが用意されています。

import { gentle, slide, snappy } from "@ssgoi/react";

const tabs = slide(undefined, {
  override: {
    forward: ({ animation }) => animation.set({ integrator: snappy }),
    backward: ({ animation }) => animation.select("in").set({ integrator: gentle }),
  },
});

旧版のコードからの移行点

2025年に書かれた日本語の解説は6系以前のAPIで、7.4以前の記事も @ssgoi/react/nextjs を前提にしています。7.5.0へ上げる場合の対応は次のとおりです。

以前の書き方 7.5.0での書き方 7.5.0での扱い
defaultTransition: fade() { priority: -100, on: "/**", transition: fade() } 型に無い(TS2353)
symmetric: true 既定で双方向。片道は bidirectional: false 型に無い(TS2353)
<SsgoiTransition id="/"> <SsgoiLayoutBoundary> 非推奨・8.0.0で削除予定
@ssgoi/react/nextjs の SsgoiRouteBoundary framework + <SsgoiLayoutBoundary> 非推奨・8.0.0で削除予定
境界の routeKey・resolve config.layouts SsgoiLayoutBoundary の引数に無い

defaultTransition と symmetric は、7.5.0の SsgoiConfig 型で tsc にかけると「Object literal may only specify known properties」で止まります。JavaScriptで書いている場合はこの検出が効かないので、設定ファイルを開いて検索してください。

一方、SsgoiRouteBoundary は7系の間は動き、tsc も通ります。型定義に @deprecated が付いているだけなので、ビルドでは気づきません。8.0.0で @ssgoi/react/nextjs と @ssgoi/react/unplugin のインポートが消える前に、framework へ移しておきます。旧 SsgoiRouteBoundary は framework を読まないので、両方を混ぜても新方式にはなりません。

View Transition APIとの違いと見送る条件

ブラウザ標準のView Transition APIは、変更前と変更後の画面をスナップショットとして撮り、疑似要素 ::view-transition の中で両者をつなぎます。同じオリジンのページ間なら、SPAにしなくても遷移を付けられます。Reactから使う場合の実装はReact 19.3のViewTransitionの解説にまとめています。

SSGOIの作者は、公式ブログ「View Transition API: Limitations and Workarounds」(2026年7月19日)で、中核に標準APIを使わない理由を説明しています。どの要素同士が対応するか、画像のどの部分が見えているかをプリセット側で計測し、連続して遷移したときの後始末まで1つのエンジンで持つためです。標準APIではスナップショットと疑似要素がブラウザの中に閉じていて、そこに手を入れにくいとしています。

一覧から詳細へのドリル、ページ全体が開くズーム、戻る操作での逆再生を複数の画面で同じように効かせたいなら、SSGOIを入れる価値があります。次の条件に当てはまるなら、見送ります。

  • フェードや見出し画像1枚の引き継ぎだけ:対象が数画面なら、依存を増やさず標準APIで書く。
  • 複数ページ構成(MPA):SSGOIは同じ文書の中でページ要素が入れ替わることが前提なので、標準APIのクロスドキュメント遷移を使う。
  • Next.js以外のルーターで、更新を追う人手が無い:連携はExperimentalで、7.0.0から7.5.0までの約3か月でルール設計とルーター接続が2度変わった。版を固定し、上げるたびに差分を読む体制が要る。
  • 端末の戻るジェスチャーや端末機能まで作り込むアプリ:iOSとAndroidのスワイプバックはOS任せなので、Flutter / React Native開発のほうが要件に合う。

ボタンやカードなど、ページ遷移ではない要素単位の動きには、Framer Motion(現Motion)のようなアニメーションライブラリが向いています。

動きを減らす設定と不具合の切り分け

prefers-reduced-motion への対応

7.5.0の @ssgoi/core と @ssgoi/react の配布ファイルには、prefers-reduced-motion を読む処理がありません。Chrome 156で prefers-reduced-motion: reduce をエミュレートして同じ遷移を試しても、アニメーションは2本とも再生されました。OSで「視差効果を減らす」を選んでいる利用者の画面も全体が動くので、プロバイダー側でルールを空にします。

// app/ssgoi-provider.tsx(framework は導入手順のまま)
import { useSyncExternalStore, type ReactNode } from "react";

const QUERY = "(prefers-reduced-motion: reduce)";

function subscribe(onChange: () => void) {
  const mql = window.matchMedia(QUERY);
  mql.addEventListener("change", onChange);
  return () => mql.removeEventListener("change", onChange);
}

const motionConfig: SsgoiConfig = {
  transitions: [
    { priority: -100, on: "/**", transition: fade() },
    { on: "/posts/**", except: "/posts", transition: drill() },
  ],
};
const reducedConfig: SsgoiConfig = { transitions: [] };

export function SsgoiProvider({ children }: { children: ReactNode }) {
  const reduce = useSyncExternalStore(
    subscribe,
    () => window.matchMedia(QUERY).matches,
    () => false,
  );
  return (
    <Ssgoi config={reduce ? reducedConfig : motionConfig} framework={framework}>
      {children}
    </Ssgoi>
  );
}

このプロバイダーに差し替えて計測すると、reduce の場合はアニメーションが0本になり、通常の設定では2本のままでした。サーバー側では false を返すので、ハイドレーションの不一致も起きません。

ルールを空にするとスクロールの扱いも変わります。公式のRoute rulesによると、どのルールにも当たらない移動では到着ページが先頭に戻り、離れたページの保存位置は破棄されます。一覧から詳細へ進んで戻ったときに一覧の位置が復元されないので、reduce の利用者がいる一覧画面は確認してから採用してください。

アニメーションしないときの確認項目

  • 移動元と移動先が、同じ1つの <Ssgoi> の下で描画されているか。プロバイダーを入れ子にしていないか。
  • 出ていくページと入ってくるページの両方に data-ssgoi-transition が付いているか。
  • 通常の <SsgoiLayoutBoundary> 方式では、境界の key がページの切り替えで変わっているか。key が同じだとReactは要素を作り直さず、付け外しが起きない。
  • その移動に当たるルールが1つ以上あるか。
  • 旧ページが新ページの下に描かれる場合は、ポータルの親に大きな z-index が付いていないか、祖先の transform・opacity で別の重なり順の文脈ができていないか。

動くのにスクロール位置がおかしい場合は、except の付け忘れを疑います。on: "/posts/**" だけだと一覧の /posts 自身も範囲に入り、一覧から詳細への移動は範囲内の移動として扱われます。この場合は両側ともスクロールが先頭に戻るため、詳細から一覧に戻ったときに元の位置が失われます。

よくある質問

SSGOIは無料で商用利用できますか?

できます。すべてのパッケージがMITライセンスで公開されており、npmから無償で導入できます。ソフトウェアの複製物または重要な部分を配布する場合は、著作権表示と許諾表示を含める必要があります。

SvelteKitやNuxtでも使えますか?

使えます。@ssgoi/svelte と @ssgoi/vue のプロバイダーに framework を渡す形で、ルールの書き方はReactと共通です。ただし、Next.js以外のルーター連携は実験的APIと明記されています。フレームワーク自体の特徴はSvelteKitの解説とNuxtの解説を参照してください。

Next.jsのPages Routerでも使えますか?

<SsgoiLayoutBoundary> はApp Router向けで、Pages Routerには対応していません。Pages Routerでは、key を付けた要素に data-ssgoi-transition を自分で書く手動の手順を使います。

Safariでも動きますか?

動きます。公式はデスクトップとモバイルの現行Safariを対象にしており、READMEにはWeb Animations APIの動作目安としてSafari 13.1以降と書かれています。

ブラウザの戻るボタンでも逆向きに動きますか?

動きます。SSGOIは直近100件の履歴について再生した効果と向きを記録し、戻る操作では同じ効果を逆向きに再生します。リロード後や記録の無い履歴への移動では、その時点のルールで判定し直します。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

RELATED POSTS 関連記事

目次