Nuxtでネイティブアプリのようなウェブアプリを作る
ウェブでアプリを作りたいなら?
ウェブ技術でアプリを作れることをご存知でしたか? Nuxtでウェブアプリを作り、ExpoやFlutterのWebViewで包めば、すぐにアプリストアにリリースできます。
しかし、ウェブアプリには最大の弱点があります。それはページ遷移が不自然ということです。
ネイティブアプリは画面遷移が自然です。リストから詳細に入るときスライドし、画像をタップすると拡大しながら開きます。でもウェブは? ページがただパッと切り替わります。
もしウェブでもネイティブのようなページ遷移が可能なら? そのウェブアプリは本物のアプリのように感じられるでしょう。
今日はNuxtでネイティブアプリのようなページトランジションが入ったウェブアプリを作ってみます。
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
# Nuxtテンプレートフォルダに移動
cd templates/nuxt
# 開発サーバーを起動
pnpm run dev
注意:
pnpm installは必ずルートパスで実行する必要があります。モノレポ構造のため、ルートで全体の依存関係をインストールします。
ブラウザでhttp://localhost:3000を開くと、テンプレートデモを確認できます。
フォルダ構造を見る
templates/nuxt/のフォルダ構造です。
templates/nuxt/
├── app.vue # ルートアプリコンポーネント
├── pages/
│ ├── posts/ # Drillトランジション例
│ │ ├── index.vue
│ │ └── [id].vue
│ ├── pinterest/ # Pinterestトランジション例
│ │ ├── index.vue
│ │ └── [id].vue
│ ├── profile/ # Instagramトランジション例
│ │ ├── index.vue
│ │ └── [id].vue
│ └── products.vue # Slideトランジション例
│ ├── all.vue
│ ├── electronics.vue
│ ├── fashion.vue
│ ├── home.vue
│ └── beauty.vue
├── components/
│ ├── demo-layout.vue # Ssgoi設定 (核心!)
│ ├── demo-wrapper.vue # モバイルフレームUI
│ ├── pin-card.vue
│ ├── post-card.vue
│ ├── profile-feed.vue
│ └── products/
└── composables/ # データ管理
├── use-posts.ts
├── use-pinterest.ts
├── use-profile.ts
└── use-products.ts
核心ファイルはcomponents/demo-layout.vueです。すべてのトランジション設定がここにあります。
Ssgoiを設定する
components/demo-layout.vueを開いてください。
<template>
<main>
<Ssgoi :config="config">
<slot />
</Ssgoi>
</main>
</template>
<script setup lang="ts">
import { Ssgoi } from '@ssgoi/vue';
import type { SsgoiConfig } from '@ssgoi/vue';
import {
drill,
pinterest,
instagram,
} from '@ssgoi/vue/view-transitions';
const config: SsgoiConfig = {
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,
},
],
};
</script>
config構成:
from: 出発パス (*はワイルドカード)to: 到着パスtransition: 使用するトランジションsymmetric: 逆方向も同じトランジションを適用
SsgoiTransitionでページを包む
各ページはSsgoiTransitionで包み、固有のidを付与する必要があります。
pages/posts/index.vueを見ると:
<template>
<SsgoiTransition id="/posts">
<div class="min-h-full bg-[#121212] px-4 py-6">
<!-- 投稿リスト -->
<div class="space-y-2">
<NuxtLink
v-for="post in posts"
:key="post.id"
:to="`/posts/${post.id}`"
>
<!-- 投稿カード -->
</NuxtLink>
</div>
</div>
</SsgoiTransition>
</template>
<script setup lang="ts">
import { SsgoiTransition } from '@ssgoi/vue';
const posts = useAllPosts();
</script>
pages/posts/[id].vueを見ると:
<template>
<SsgoiTransition :id="`/posts/${id}`">
<div class="min-h-screen bg-[#121212]">
<!-- 投稿詳細 -->
</div>
</SsgoiTransition>
</template>
<script setup lang="ts">
import { SsgoiTransition } from '@ssgoi/vue';
const route = useRoute();
const id = route.params.id as string;
const post = usePost(id);
</script>
核心:
idはconfigのfrom/toと一致する必要があります。SSGOIがトランジションを適用するためです。
Drillトランジション
リストから詳細に入る感じのトランジションです。

テンプレート位置: pages/posts/
import { drill } from '@ssgoi/vue/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で左右にスライドする効果です。

テンプレート位置: pages/products.vue
import { slide } from '@ssgoi/vue/view-transitions';
// 左にスライド
slide({ direction: 'left' });
// 右にスライド
slide({ direction: 'right' });
pages/products.vueでは、ネストされたSsgoiを使用してタブエリアのみをトランジションします:
<template>
<SsgoiTransition id="/products">
<!-- ヘッダー、タブボタン -->
<div class="flex-1 overflow-hidden">
<Ssgoi :config="config">
<NuxtPage />
</Ssgoi>
</div>
</SsgoiTransition>
</template>
<script setup lang="ts">
import { Ssgoi, SsgoiTransition } from '@ssgoi/vue';
import type { SsgoiConfig } from '@ssgoi/vue';
import { slide } from '@ssgoi/vue/view-transitions';
const categories = [
{ id: 'all', label: 'All', path: '/products/all' },
{ id: 'electronics', label: 'Tech', path: '/products/electronics' },
{ id: 'fashion', label: 'Fashion', path: '/products/fashion' },
{ id: 'home', label: 'Home', path: '/products/home' },
{ id: 'beauty', label: 'Beauty', path: '/products/beauty' },
];
const config: SsgoiConfig = {
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' }),
},
],
middleware: (from: string, to: string) => {
const fromIndex = categories.findIndex((c) => c.path === from);
const toIndex = categories.findIndex((c) => c.path === to);
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
if (fromIndex < toIndex) {
return { from: '/products/tab/left', to: '/products/tab/right' };
} else {
return { from: '/products/tab/right', to: '/products/tab/left' };
}
}
return { from, to };
},
};
</script>
Pinterestトランジション
ギャラリー画像が拡大しながら詳細に遷移する効果です。

テンプレート位置: pages/pinterest/
Data属性設定 (必須!)
Pinterestトランジションはdata属性でどの画像同士が接続されるかを指定する必要があります。
components/pin-card.vue (ギャラリー):
<template>
<NuxtLink :to="`/pinterest/${item.id}`">
<div class="relative" :style="{ aspectRatio: item.aspectRatio }">
<img
:src="item.image"
:alt="item.title"
class="w-full h-full object-cover"
:data-pinterest-gallery-key="item.id"
/>
</div>
</NuxtLink>
</template>
<script setup lang="ts">
defineProps<{
item: PinterestItem;
}>();
</script>
pages/pinterest/[id].vue (詳細):
<template>
<SsgoiTransition :id="`/pinterest/${id}`">
<div v-if="item">
<img
:src="item.image"
:alt="item.title"
:style="{ aspectRatio: item.aspectRatio }"
:data-pinterest-detail-key="item.id"
/>
</div>
</SsgoiTransition>
</template>
<script setup lang="ts">
import { SsgoiTransition } from '@ssgoi/vue';
const route = useRoute();
const id = route.params.id as string;
const item = usePinterestItem(id);
</script>
核心:
data-pinterest-gallery-keyとdata-pinterest-detail-keyに同じidを入れることで、SSGOIが接続を認識します。
config設定:
{
from: '/pinterest/*',
to: '/pinterest',
transition: pinterest(),
symmetric: true,
}
Instagramトランジション
プロフィールフィードグリッドから詳細に遷移する効果です。Pinterestと似ていますが、ギャラリーがそのまま維持されます。

テンプレート位置: pages/profile/
Data属性設定
components/post-card.vue (グリッド):
<template>
<NuxtLink :to="`/profile/${post.id}`">
<img
:src="post.coverImage.url"
:alt="post.title"
class="w-full h-auto object-cover"
:data-instagram-gallery-key="post.id"
/>
</NuxtLink>
</template>
<script setup lang="ts">
defineProps<{
post: ProfilePost;
}>();
</script>
pages/profile/[id].vue (詳細):
<template>
<SsgoiTransition :id="`/profile/${id}`">
<div v-if="post">
<img
:src="post.coverImage.url"
:alt="post.title"
:data-instagram-detail-key="post.id"
/>
</div>
</SsgoiTransition>
</template>
<script setup lang="ts">
import { SsgoiTransition } from '@ssgoi/vue';
const route = useRoute();
const id = route.params.id as string;
const post = useProfilePost(id);
</script>
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オプション
双方向トランジションを自動設定します。
// このように1つだけ設定すれば
{
from: '/pinterest/*',
to: '/pinterest',
transition: pinterest(),
symmetric: true,
}
// 逆方向も自動適用されます
ワイルドカードルート
*ですべてのサブパスをマッチングします。
// /posts/1, /posts/abc すべてマッチ
{ from: '/posts', to: '/posts/*', ... }
まとめ
これで皆さんもNuxtウェブアプリにネイティブのようなページトランジションを適用できます!
リソース
- GitHub: https://github.com/meursyphus/ssgoi
- 公式ドキュメント: https://ssgoi.dev
- Nuxtテンプレート: https://github.com/meursyphus/ssgoi/tree/main/templates/nuxt
WebViewで包んで本物のアプリにしてみてください!