Next.jsでネイティブアプリのようなWebアプリを作る
Webでアプリを作りたいですか?
Web技術でアプリを作れることをご存知でしたか? Next.jsでWebアプリを作り、ExpoやFlutterのWebViewでラップすれば、すぐにApp Storeにリリースできます。
しかし、Webアプリには最も大きな弱点があります。それはページ遷移が不自然であることです。
ネイティブアプリは画面遷移が自然です。リストから詳細に入る時はスライドし、画像をタップすると拡大しながら開きます。しかしWebは? ページがただパッと変わるだけです。
もしWebでもネイティブのようなページ遷移が可能だったら? そのWebアプリは本物のアプリのように感じられるでしょう。
今日はNext.jsでネイティブアプリのようなページトランジションを持つWebアプリを作ります。
SSGOIとは?
SSGOI(쓱오이)はすべてのブラウザで動作するページトランジションライブラリです。
ChromeのView Transition APIがありますが、SafariとFirefoxでは動作しません。SSGOIはこの問題を解決します。
SSGOIの利点:
- すべてのモダンブラウザをサポート (Chrome, Firefox, Safari, Edge)
- SSR/SSG完全互換
- スプリングベースの物理アニメーションで自然な動き
- 多様なビルトイントランジション (Drill, Slide, Pinterest, Instagramなど)
始める
SSGOI公式テンプレートから始めましょう。
# リポジトリをクローン
git clone https://github.com/meursyphus/ssgoi
# ルートで依存関係をインストール (重要!)
pnpm install
# Next.jsテンプレートフォルダに移動
cd templates/nextjs
# 開発サーバーを起動
pnpm run dev
注意:
pnpm installは必ずルートパスで実行する必要があります。モノレポ構造のため、ルートで全ての依存関係をインストールします。
ブラウザでhttp://localhost:3000を開くと、テンプレートデモを確認できます。
フォルダ構造を見る
templates/nextjs/フォルダの構造です。
templates/nextjs/
├── src/
│ ├── app/
│ │ ├── layout.tsx # ルートレイアウト
│ │ ├── posts/ # Drillトランジション例
│ │ │ ├── page.tsx
│ │ │ └── [id]/page.tsx
│ │ ├── pinterest/ # Pinterestトランジション例
│ │ │ ├── page.tsx
│ │ │ └── [id]/page.tsx
│ │ ├── profile/ # Instagramトランジション例
│ │ │ ├── page.tsx
│ │ │ └── [id]/page.tsx
│ │ └── products/ # Slideトランジション例
│ │ ├── layout.tsx
│ │ └── [category]/page.tsx
│ └── components/
│ ├── demo-layout.tsx # Ssgoi設定 (核心!)
│ ├── demo-wrapper.tsx # モバイルフレームUI
│ ├── posts/
│ ├── pinterest/
│ ├── profile/
│ └── products/
核心ファイルはdemo-layout.tsxです。すべてのトランジション設定がここにあります。
Ssgoiを設定する
src/components/demo-layout.tsxを開いてください。
"use client";
import { Ssgoi } from "@ssgoi/react";
import { drill, pinterest, instagram } from "@ssgoi/react/view-transitions";
import { useMemo } from "react";
export default function DemoLayout({
children,
}: {
children: React.ReactNode;
}) {
const config = useMemo(
() => ({
transitions: [
// Pinterestトランジション
{
from: "/pinterest/*",
to: "/pinterest",
transition: pinterest(),
symmetric: true,
},
// Posts - Drillトランジション
{
from: "/posts",
to: "/posts/*",
transition: drill({ direction: "enter" }),
},
{
from: "/posts/*",
to: "/posts",
transition: drill({ direction: "exit" }),
},
// Profile - Instagramトランジション
{
from: "/profile",
to: "/profile/*",
transition: instagram(),
symmetric: true,
},
],
}),
[],
);
return (
<main>
<Ssgoi config={config}>{children}</Ssgoi>
</main>
);
}
config構成:
from: 出発パス (*はワイルドカード)to: 到着パスtransition: 使用するトランジションsymmetric: 逆方向にも同じトランジションを適用
SsgoiTransitionでページをラップする
各ページはSsgoiTransitionでラップし、一意のidを付与する必要があります。
src/components/posts/index.tsxを見ると:
import { SsgoiTransition } from "@ssgoi/react";
export default function PostsDemo() {
return (
<SsgoiTransition id="/posts">
<div className="min-h-screen bg-[#121212] px-4 py-6">
{/* 投稿リスト */}
</div>
</SsgoiTransition>
);
}
src/components/posts/detail.tsxを見ると:
export default function PostDetail({ postId }: { postId: string }) {
return (
<SsgoiTransition id={`/posts/${postId}`}>
<div className="min-h-screen bg-[#121212]">{/* 投稿詳細 */}</div>
</SsgoiTransition>
);
}
核心:
idはconfigのfrom/toと一致する必要があり、SSGOIがトランジションを適用します。
Drillトランジション
リストから詳細に入る感じのトランジションです。

テンプレート位置: src/components/posts/
import { drill } from "@ssgoi/react/view-transitions";
// リスト → 詳細 (入る時)
drill({ direction: "enter" });
// 詳細 → リスト (出る時)
drill({ direction: "exit" });
config設定:
{
from: "/posts",
to: "/posts/*",
transition: drill({ direction: "enter" }),
},
{
from: "/posts/*",
to: "/posts",
transition: drill({ direction: "exit" }),
},
オプション:
direction:"enter"|"exit"opacity:trueならフェード効果を追加
Slideトランジション
タブUIで左右にスライドする効果です。

テンプレート位置: src/app/products/layout.tsx
import { slide } from "@ssgoi/react/view-transitions";
// 左にスライド
slide({ direction: "left" });
// 右にスライド
slide({ direction: "right" });
products/layout.tsxでは、ネストされたSsgoiを使用してタブ領域のみトランジションします:
export default function ProductsLayout({
children,
}: {
children: React.ReactNode;
}) {
const config = useMemo(
() => ({
transitions: [
{
from: "/products/tab/left",
to: "/products/tab/right",
transition: slide({ direction: "left" }),
},
{
from: "/products/tab/right",
to: "/products/tab/left",
transition: slide({ direction: "right" }),
},
],
}),
[],
);
return (
<SsgoiTransition id="/products">
{/* ヘッダー、タブボタン */}
<div className="flex-1 overflow-hidden">
<Ssgoi config={config}>{children}</Ssgoi>
</div>
</SsgoiTransition>
);
}
Pinterestトランジション
ギャラリー画像が拡大しながら詳細に遷移する効果です。

テンプレート位置: src/components/pinterest/
Data属性を設定 (必須!)
Pinterestトランジションはdata属性でどの画像同士が接続されるかを指定する必要があります。
src/components/pinterest/index.tsx (ギャラリー):
function PinCard({ item }: { item: PinterestItem }) {
return (
<Link href={`/pinterest/${item.id}`}>
<div className="relative" style={{ aspectRatio: item.aspectRatio }}>
<img
src={item.image}
alt={item.title}
className="w-full h-full object-cover"
data-pinterest-gallery-key={item.id}
/>
</div>
</Link>
);
}
src/components/pinterest/detail.tsx (詳細):
export default function PinterestDetail({ pinId }: { pinId: string }) {
const item = getPinterestItem(pinId);
return (
<SsgoiTransition id={`/pinterest/${pinId}`}>
<img
src={item.image}
alt={item.title}
style={{ aspectRatio: item.aspectRatio }}
data-pinterest-detail-key={item.id}
/>
</SsgoiTransition>
);
}
核心:
data-pinterest-gallery-keyとdata-pinterest-detail-keyに同じidを入れることで、SSGOIが接続を認識します。
config設定:
{
from: "/pinterest/*",
to: "/pinterest",
transition: pinterest(),
symmetric: true,
}
Instagramトランジション
プロフィールフィードグリッドから詳細に遷移する効果です。Pinterestに似ていますが、ギャラリーがそのまま維持されます。

テンプレート位置: src/components/profile/
Data属性を設定
src/components/profile/feed.tsx (グリッド):
function PostCard({ post }: { post: Post }) {
return (
<Link href={`/profile/${post.id}`}>
<img
src={post.coverImage.url}
alt={post.title}
className="w-full h-auto object-cover"
data-instagram-gallery-key={post.id}
/>
</Link>
);
}
src/components/profile/feed-detail.tsx (詳細):
export default function FeedDetail({ postId }: { postId: string }) {
const post = getPost(postId);
return (
<SsgoiTransition id={`/profile/${postId}`}>
<img
src={post.coverImage.url}
alt={post.title}
data-instagram-detail-key={post.id}
/>
</SsgoiTransition>
);
}
config設定:
{
from: "/profile",
to: "/profile/*",
transition: instagram(),
symmetric: true,
}
Springオプションでタイミングを調整する
すべてのトランジションはスプリングベースの物理アニメーションを使用します。springオプションで速度と感触を調整できます。
drill({
direction: "enter",
spring: {
stiffness: 200, // 高いほど速い
damping: 20, // 高いほどバウンスが少ない
doubleSpring: true, // ease-in-out効果をオン
},
});
Springオプションの説明
| オプション | 説明 | 効果 |
|---|---|---|
stiffness | 剛性 | 高いほど速く即座 |
damping | 減衰 | 高いほどバウンスが少なく滑らか |
doubleSpring | ダブルスプリング | true/false - ease-in-out効果 |
doubleSpringとは? CSSのease-in-outのように、始まりと終わりが滑らかな効果です。詳細はDouble Springブログ記事を参照してください。
便利なオプション
symmetricオプション
双方向トランジションを自動設定します。
// このように一つだけ設定すれば
{
from: "/pinterest/*",
to: "/pinterest",
transition: pinterest(),
symmetric: true,
}
// 逆方向も自動適用されます
ワイルドカードルート
*ですべてのサブパスをマッチングします。
// /posts/1, /posts/abc すべてマッチング
{ from: "/posts", to: "/posts/*", ... }
まとめ
これで皆さんもNext.js Webアプリにネイティブのようなページトランジションを適用できます!
リソース
- GitHub: https://github.com/meursyphus/ssgoi
- 公式ドキュメント: https://ssgoi.dev
- Next.jsテンプレート: https://github.com/meursyphus/ssgoi/tree/main/templates/nextjs
WebViewでラップして本物のアプリにしてみてください!