SSGOI LogoSSGOI
← ブログに戻る
Nuxtでネイティブアプリのようなウェブアプリを作る

Nuxtでネイティブアプリのようなウェブアプリを作る

文大承
ssgoinuxtvuepage-transitiontutorial

ウェブでアプリを作りたいなら?

ウェブ技術でアプリを作れることをご存知でしたか? 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トランジション

リストから詳細に入る感じのトランジションです。

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で左右にスライドする効果です。

Slideトランジション - タブクリック時に左右スライド

テンプレート位置: 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トランジション

ギャラリー画像が拡大しながら詳細に遷移する効果です。

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-keydata-pinterest-detail-key同じidを入れることで、SSGOIが接続を認識します。

config設定:

{
  from: '/pinterest/*',
  to: '/pinterest',
  transition: pinterest(),
  symmetric: true,
}

Instagramトランジション

プロフィールフィードグリッドから詳細に遷移する効果です。Pinterestと似ていますが、ギャラリーがそのまま維持されます。

Instagramトランジション - グリッドから詳細に遷移、ギャラリー維持

テンプレート位置: 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ウェブアプリにネイティブのようなページトランジションを適用できます!

リソース

WebViewで包んで本物のアプリにしてみてください!