SSGOI LogoSSGOI
← ブログに戻る
Next.jsでネイティブアプリのようなWebアプリを作る

Next.jsでネイティブアプリのようなWebアプリを作る

文大承
ssgoinextjsreactpage-transitiontutorial

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トランジション

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

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

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

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

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

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

config設定:

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

Instagramトランジション

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

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

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

リソース

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