T-CREATOR

Astro の View Transitions 徹底解説:SPA 並みの滑らかなページ遷移を実装するコツ

Astro の View Transitions 徹底解説:SPA 並みの滑らかなページ遷移を実装するコツ

従来の Web サイトでは、ページ遷移時に画面全体が再読み込みされ、ユーザー体験が途切れてしまうことが課題でした。一方、Astro の View Transitions を活用すると、JavaScript フレームワークに頼らず、驚くほどスムーズなページ遷移が実現できます。

この記事では、Astro の View Transitions API を使って SPA(Single Page Application)並みの滑らかなページ遷移を実装する方法を、初心者の方にもわかりやすく徹底解説していきます。基本的な設定から高度なカスタマイズまで、実践的なコード例とともにご紹介しますので、ぜひ最後までお読みください。

背景

Web ページ遷移の進化

Web サイトのページ遷移は、長年にわたって進化を続けてきました。従来の MPA(Multi-Page Application)では、リンクをクリックするたびにブラウザが新しい HTML ドキュメント全体を読み込み、画面が一瞬白くなる現象が発生していました。

この課題を解決するため、React や Vue.js などの JavaScript フレームワークを使った SPA が登場しました。SPA では、必要な部分だけを動的に更新することで、滑らかなページ遷移を実現できます。しかし、JavaScript のバンドルサイズが大きくなり、初期表示速度が遅くなるというトレードオフがありました。

View Transitions API の登場

2023 年、Chrome をはじめとするモダンブラウザに View Transitions API が実装されました。これは、ページ遷移時にアニメーションを適用できる新しいブラウザ標準 API です。

以下の図は、従来の MPA と View Transitions を適用した場合の遷移フローを示しています。

mermaidflowchart LR
  subgraph traditional["従来の MPA"]
    page1["ページ A"] -->|クリック| reload["画面が白く<br/>なる"]
    reload -->|再読み込み| page2["ページ B"]
  end

  subgraph vt["View Transitions"]
    pageA["ページ A"] -->|クリック| animate["スムーズな<br/>アニメーション"]
    animate -->|遷移| pageB["ページ B"]
  end

従来の MPA では一度画面が白くなりますが、View Transitions では途切れることなくスムーズに遷移します。

Astro における View Transitions のサポート

Astro は、Astro 3.0 から View Transitions API を公式にサポートしています。Astro の View Transitions には、以下の特徴があります。

#特徴説明
1ゼロ JavaScriptネイティブ API を活用し、最小限の JavaScript で動作
2フォールバック機能未対応ブラウザでも動作する代替機能を提供
3カスタマイズ性アニメーションやタイミングを細かく制御可能
4アクセシビリティprefers-reduced-motion に対応

Astro は、静的サイトジェネレーターとしての高速性を保ちながら、SPA のような滑らかな UX を提供できる点が大きな魅力です。

近年、Core Web Vitals などのパフォーマンス指標が SEO にも影響するようになり、高速かつ滑らかな Web サイトの需要が高まっています。Astro の View Transitions は、パフォーマンスと UX の両立を実現する強力なソリューションといえるでしょう。

課題

従来のページ遷移における問題点

Web サイトを構築する際、ページ遷移の実装には複数の課題が存在します。開発者は、ユーザー体験とパフォーマンスのバランスを取ることに苦労してきました。

MPA の課題

従来の MPA では、ページ遷移のたびに以下の問題が発生します。

  • 画面全体が再描画されるため、視覚的な連続性が失われる
  • ヘッダーやナビゲーションなどの共通要素も再読み込みされる
  • ページ読み込み中に画面が白くなり、ユーザーが不安を感じる
  • CSS や JavaScript ファイルが毎回再ダウンロードされる可能性がある

SPA の課題

一方、JavaScript フレームワークを使った SPA にも課題があります。

#課題詳細
1バンドルサイズJavaScript のサイズが大きくなり、初期表示が遅延
2SEO の複雑化クライアントサイドレンダリングによる検索エンジン対応
3複雑な状態管理ルーティングや状態の管理に高度な知識が必要
4開発コストフレームワークの学習コストと保守コストが高い

これらの課題により、開発者は「シンプルな静的サイト」と「リッチな SPA」のどちらを選ぶべきか、という難しい選択を迫られてきました。

ブラウザ互換性の問題

View Transitions API は比較的新しい技術のため、ブラウザのサポート状況にばらつきがあります。Chromium ベースのブラウザ(Chrome、Edge など)では完全にサポートされていますが、Safari や Firefox では段階的な対応が進められています。

以下の図は、View Transitions を実装する際のブラウザ対応フローです。

mermaidflowchart TD
  start["ページ遷移開始"] --> check["ブラウザが<br/>View Transitions<br/>をサポート?"]
  check -->|Yes| native["ネイティブ API<br/>でアニメーション"]
  check -->|No| fallback["フォールバック<br/>処理"]
  native --> done["遷移完了"]
  fallback --> swap["即座に<br/>ページ切り替え"]
  swap --> done

ブラウザがネイティブに対応していない場合、フォールバック処理が必要になります。

アニメーションのカスタマイズの難しさ

デフォルトのフェードアニメーションは便利ですが、ブランドイメージやデザインに合わせたカスタマイズが必要な場合もあります。

しかし、以下のような課題があります。

  • CSS のアニメーション知識が必要
  • 複数ページ間で一貫したアニメーションを実装する手間
  • パフォーマンスへの影響を考慮した最適化
  • アクセシビリティへの配慮(視覚障害やアニメーションに敏感なユーザーへの対応)

Astro の View Transitions は、これらの課題を解決するための機能を提供していますが、その使い方を理解することが重要です。

解決策

Astro View Transitions の導入

Astro の View Transitions を使えば、上記の課題を効果的に解決できます。ここでは、基本的な導入方法から高度なカスタマイズまで、段階的に解説していきます。

基本的なセットアップ

Astro で View Transitions を有効にするには、<ClientRouter ​/​> コンポーネントを共通レイアウトの <head> タグ内に追加するだけです。

まず、必要なコンポーネントをインポートします。

typescript---
// src/layouts/BaseLayout.astro
import { ClientRouter } from 'astro:transitions';
---

次に、レイアウトファイルの <head> 内に <ClientRouter ​/​> を配置します。

astro<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>My Astro Site</title>

    <!-- View Transitions を有効化 -->
    <ClientRouter />
  </head>
  <body>
    <slot />
  </body>
</html>

このシンプルな設定だけで、サイト全体のページ遷移にフェードアニメーションが適用されます。デフォルトでは、古いページがフェードアウトしながら新しいページがフェードインする演出になります。

以下の図は、ClientRouter が有効になったときの内部動作フローです。

mermaidsequenceDiagram
  participant User as ユーザー
  participant Browser as ブラウザ
  participant ClientRouter as ClientRouter
  participant DOM as DOM

  User->>Browser: リンクをクリック
  Browser->>ClientRouter: 遷移をインターセプト
  ClientRouter->>DOM: 現在のページの<br/>スナップショット作成
  ClientRouter->>Browser: 新しいページを<br/>フェッチ
  Browser->>ClientRouter: HTML を返却
  ClientRouter->>DOM: View Transition<br/>API を起動
  DOM->>DOM: アニメーション実行
  DOM->>User: 新しいページを表示

ClientRouter がリンクのクリックを検知し、View Transitions API を呼び出すことで、スムーズなアニメーションを実現しています。

ビルトインアニメーションの活用

Astro には、以下の 3 つのビルトインアニメーションが用意されています。

#アニメーション名効果推奨用途
1fadeフェードイン・フェードアウト汎用的な画面遷移
2slideスライドイン・スライドアウトナビゲーション、モーダル
3noneアニメーションなし即座の切り替えが必要な場合

これらのアニメーションは、transition:animate ディレクティブを使って個別の要素に適用できます。

以下は、メインコンテンツにスライドアニメーションを適用する例です。

astro---
// src/pages/about.astro
import BaseLayout from '../layouts/BaseLayout.astro';
---

<BaseLayout>
  <!-- スライドアニメーションを適用 -->
  <main transition:animate="slide">
    <h1>About Us</h1>
    <p>会社情報をご紹介します。</p>
  </main>
</BaseLayout>

この設定により、ページ遷移時にメインコンテンツがスライドイン・スライドアウトします。

transition ディレクティブによる要素の対応付け

transition:name ディレクティブを使うと、異なるページ間で同じ要素を対応付けることができます。これにより、特定の要素が滑らかに変形・移動するアニメーションを実現できます。

基本的な使い方

たとえば、ブログ記事の一覧ページと詳細ページで、同じ画像を使っている場合を考えましょう。

一覧ページ(src​/​pages​/​blog​/​index.astro)では、以下のように記述します。

astro<a href="/blog/my-first-post">
  <img
    src="/images/post-1.jpg"
    alt="記事のサムネイル"
    transition:name="post-image-1"
  />
  <h2>私の最初の投稿</h2>
</a>

詳細ページ(src​/​pages​/​blog​/​my-first-post.astro)でも、同じ transition:name を指定します。

astro<article>
  <img
    src="/images/post-1.jpg"
    alt="記事のメイン画像"
    transition:name="post-image-1"
  />
  <h1>私の最初の投稿</h1>
  <div class="content">
    <!-- 記事本文 -->
  </div>
</article>

このように同じ transition:name を指定することで、一覧ページの小さいサムネイル画像が、詳細ページの大きなメイン画像へと滑らかに変形・移動します。

動的な要素への適用

ブログ記事が複数ある場合、動的に transition:name を生成することもできます。

astro---
// src/pages/blog/index.astro
const posts = await fetchPosts(); // 記事データを取得
---

<div class="post-grid">
  {posts.map((post) => (
    <a href={`/blog/${post.slug}`}>
      <img
        src={post.image}
        alt={post.title}
        transition:name={`post-image-${post.id}`}
      />
      <h2>{post.title}</h2>
    </a>
  ))}
</div>

各記事の ID を使って一意な transition:name を生成することで、どの記事をクリックしても適切なアニメーションが適用されます。

transition ディレクティブで状態を保持

transition:persist ディレクティブを使うと、ページ遷移後も特定の要素の状態を維持できます。これは、動画プレーヤーや音楽プレーヤーなど、ページをまたいで動作を継続させたい要素に有効です。

動画プレーヤーの状態を保持

以下は、ページ遷移後も動画の再生を継続する例です。

astro---
// src/components/VideoPlayer.astro
---

<div class="video-container" transition:persist>
  <video
    id="main-video"
    src="/videos/intro.mp4"
    controls
    autoplay
  >
    ご利用のブラウザは動画タグをサポートしていません。
  </video>
</div>

<style>
  .video-container {
    /* スタイリング */
    width: 100%;
    max-width: 800px;
    margin: 0 auto;
  }
</style>

transition:persist を指定した要素は、ページ遷移後も DOM から削除されず、そのまま保持されます。そのため、動画の再生位置やプレーヤーの状態がリセットされません。

オーディオプレーヤーの実装例

同様に、サイト全体で共通のバックグラウンドミュージックを再生する場合も、以下のように実装できます。

astro---
// src/components/AudioPlayer.astro
---

<audio
  transition:persist
  id="bg-music"
  src="/audio/background.mp3"
  loop
  autoplay
>
  ご利用のブラウザはオーディオタグをサポートしていません。
</audio>

この設定により、ユーザーがページを移動しても音楽が途切れることなく再生され続けます。

カスタムアニメーションの作成

ビルトインアニメーションだけでは物足りない場合、CSS を使って独自のアニメーションを定義できます。

カスタムアニメーションの定義

まず、グローバル CSS ファイルにキーフレームアニメーションを定義します。

css/* src/styles/transitions.css */

/* スライドアップアニメーション */
@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* スライドダウンアニメーション(退出時) */
@keyframes slideDown {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(30px);
  }
}

次に、::view-transition 疑似要素を使ってアニメーションを適用します。

css/* 新しいページの要素に適用 */
::view-transition-new(main) {
  animation: slideUp 0.4s ease-out;
}

/* 古いページの要素に適用 */
::view-transition-old(main) {
  animation: slideDown 0.4s ease-in;
}

レイアウトファイルで、この CSS を読み込みます。

astro---
// src/layouts/BaseLayout.astro
import { ClientRouter } from 'astro:transitions';
import '../styles/transitions.css';
---

<html lang="ja">
  <head>
    <ClientRouter />
  </head>
  <body>
    <main transition:name="main">
      <slot />
    </main>
  </body>
</html>

これにより、メインコンテンツがスライドアップ・スライドダウンのアニメーションで遷移します。

イージング関数のカスタマイズ

アニメーションの雰囲気を変えるには、イージング関数を調整します。

css/* src/styles/transitions.css */

/* バウンス効果のあるアニメーション */
::view-transition-new(main) {
  animation: slideUp 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}

/* スムーズな減速 */
::view-transition-old(main) {
  animation: slideDown 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

イージング関数を工夫することで、ブランドイメージに合った独自のアニメーション表現が可能になります。

フォールバック戦略の実装

View Transitions API に対応していないブラウザでも、適切なフォールバック処理を実装することで、すべてのユーザーに快適な体験を提供できます。

フォールバックオプションの設定

<ClientRouter ​/​> コンポーネントには、fallback プロパティが用意されています。

astro---
// src/layouts/BaseLayout.astro
import { ClientRouter } from 'astro:transitions';
---

<html lang="ja">
  <head>
    <!-- フォールバック設定 -->
    <ClientRouter fallback="swap" />
  </head>
  <body>
    <slot />
  </body>
</html>

fallback プロパティには、以下のオプションがあります。

#オプション動作推奨ケース
1animateCSS によるシンプルなアニメーションデフォルト設定
2swap即座にページを切り替えパフォーマンス重視
3noneブラウザのデフォルト動作トラブルシューティング時

JavaScript によるブラウザ判定

より細かい制御が必要な場合、JavaScript でブラウザの対応状況を判定できます。

typescript// src/scripts/check-support.ts

/**
 * View Transitions API のサポート状況をチェック
 */
export function supportsViewTransitions(): boolean {
  return (
    typeof document !== 'undefined' &&
    'startViewTransition' in document
  );
}

// 使用例
if (supportsViewTransitions()) {
  console.log(
    'View Transitions API がサポートされています'
  );
} else {
  console.log('フォールバック処理を使用します');
}

このヘルパー関数を使うことで、条件分岐や機能の段階的な提供が可能になります。

アクセシビリティへの配慮

View Transitions を実装する際は、アクセシビリティにも十分な配慮が必要です。

prefers-reduced-motion への対応

アニメーションに敏感なユーザーや、視覚障害のあるユーザーのために、prefers-reduced-motion メディアクエリに対応します。

css/* src/styles/transitions.css */

/* デフォルトのアニメーション */
::view-transition-new(root) {
  animation: fade-in 0.4s ease-out;
}

/* アニメーションを減らす設定の場合 */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-new(root),
  ::view-transition-old(root) {
    animation-duration: 0.01s !important;
    animation-iteration-count: 1 !important;
  }
}

この設定により、ユーザーの OS やブラウザで「アニメーションを減らす」設定が有効になっている場合、アニメーションがほぼ瞬時に完了します。

スクリーンリーダーへの対応

Astro の View Transitions は、自動的にルート変更をスクリーンリーダーに通知します。追加の設定は不要ですが、必要に応じて以下のような aria-live 領域を設定できます。

astro---
// src/components/RouteAnnouncer.astro
---

<div
  class="sr-only"
  aria-live="polite"
  aria-atomic="true"
  id="route-announcer"
>
  <!-- ページ遷移時にここに通知が表示されます -->
</div>

<style>
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
  }
</style>

この要素は視覚的には非表示ですが、スクリーンリーダーには読み上げられます。

具体例

ここでは、実際のプロジェクトで使える具体的な実装例を紹介します。コードをコピーして、そのまま使えるように詳しく解説していきます。

ブログサイトでの実装例

ブログサイトは、View Transitions を活用するのに最適なユースケースです。記事一覧から詳細ページへの遷移を滑らかにすることで、読者の体験が大きく向上します。

プロジェクト構成

まず、基本的なディレクトリ構成を確認しましょう。

csssrc/
├── layouts/
│   └── BaseLayout.astro
├── pages/
│   ├── index.astro
│   ├── blog/
│   │   ├── index.astro
│   │   └── [slug].astro
├── components/
│   └── BlogCard.astro
└── styles/
    └── global.css

共通レイアウトの作成

すべてのページで使用する基本レイアウトを作成します。

astro---
// src/layouts/BaseLayout.astro
import { ClientRouter } from 'astro:transitions';

interface Props {
  title: string;
  description?: string;
}

const { title, description } = Astro.props;
---

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <meta name="description" content={description} />
    <title>{title}</title>

    <!-- View Transitions を有効化 -->
    <ClientRouter />

    <link rel="stylesheet" href="/styles/global.css" />
  </head>
  <body>
    <!-- ヘッダーは常に表示 -->
    <header transition:persist>
      <nav>
        <a href="/">Home</a>
        <a href="/blog">Blog</a>
        <a href="/about">About</a>
      </nav>
    </header>

    <!-- メインコンテンツ -->
    <main transition:animate="slide">
      <slot />
    </main>

    <!-- フッターも常に表示 -->
    <footer transition:persist>
      <p>&copy; 2025 My Blog</p>
    </footer>
  </body>
</html>

このレイアウトでは、ヘッダーとフッターに transition:persist を指定しているため、ページ遷移時も固定表示されます。メインコンテンツにはスライドアニメーションを適用しています。

ブログカードコンポーネント

記事一覧で使用するカードコンポーネントを作成します。

astro---
// src/components/BlogCard.astro
interface Props {
  id: number;
  slug: string;
  title: string;
  image: string;
  excerpt: string;
  date: string;
}

const { id, slug, title, image, excerpt, date } = Astro.props;
---

<article class="blog-card">
  <a href={`/blog/${slug}`}>
    <!-- 画像に一意の transition:name を設定 -->
    <img
      src={image}
      alt={title}
      transition:name={`blog-image-${id}`}
      class="card-image"
    />

    <!-- タイトルにも transition:name を設定 -->
    <h2 transition:name={`blog-title-${id}`}>
      {title}
    </h2>

    <p class="excerpt">{excerpt}</p>

    <time datetime={date}>{date}</time>
  </a>
</article>

<style>
  .blog-card {
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    overflow: hidden;
    transition: transform 0.2s;
  }

  .blog-card:hover {
    transform: translateY(-4px);
  }

  .card-image {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }

  .excerpt {
    color: #666;
    line-height: 1.6;
  }
</style>

画像とタイトルに一意な transition:name を設定することで、一覧ページと詳細ページ間でスムーズな変形アニメーションが適用されます。

ブログ一覧ページ

記事一覧を表示するページを作成します。

astro---
// src/pages/blog/index.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import BlogCard from '../../components/BlogCard.astro';

// ブログ記事データを取得
const posts = [
  {
    id: 1,
    slug: 'getting-started-with-astro',
    title: 'Astro を始めよう',
    image: '/images/post-1.jpg',
    excerpt: 'Astro の基本的な使い方を解説します。',
    date: '2025-01-15'
  },
  {
    id: 2,
    slug: 'view-transitions-guide',
    title: 'View Transitions 完全ガイド',
    image: '/images/post-2.jpg',
    excerpt: 'View Transitions の実装方法を詳しく紹介します。',
    date: '2025-02-20'
  },
  // その他の記事...
];
---

<BaseLayout
  title="Blog - My Site"
  description="技術ブログの記事一覧"
>
  <div class="blog-container">
    <h1>Blog</h1>

    <div class="blog-grid">
      {posts.map((post) => (
        <BlogCard {...post} />
      ))}
    </div>
  </div>
</BaseLayout>

<style>
  .blog-container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
  }

  .blog-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 2rem;
    margin-top: 2rem;
  }
</style>

グリッドレイアウトで記事カードを並べています。レスポンシブ対応も考慮した実装です。

ブログ詳細ページ

個別の記事ページを作成します。

astro---
// src/pages/blog/[slug].astro
import BaseLayout from '../../layouts/BaseLayout.astro';

// 実際にはデータベースや CMS から取得
const { slug } = Astro.params;

const post = {
  id: 1,
  title: 'Astro を始めよう',
  image: '/images/post-1.jpg',
  content: '<p>Astro は素晴らしいフレームワークです...</p>',
  date: '2025-01-15',
  author: '山田太郎'
};
---

<BaseLayout
  title={post.title}
  description={post.title}
>
  <article class="post-detail">
    <!-- 一覧ページと同じ transition:name を指定 -->
    <img
      src={post.image}
      alt={post.title}
      transition:name={`blog-image-${post.id}`}
      class="hero-image"
    />

    <div class="post-header">
      <h1 transition:name={`blog-title-${post.id}`}>
        {post.title}
      </h1>

      <div class="post-meta">
        <time datetime={post.date}>{post.date}</time>
        <span>by {post.author}</span>
      </div>
    </div>

    <div class="post-content" set:html={post.content} />
  </article>
</BaseLayout>

<style>
  .post-detail {
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
  }

  .hero-image {
    width: 100%;
    height: 400px;
    object-fit: cover;
    border-radius: 8px;
  }

  .post-header {
    margin: 2rem 0;
  }

  .post-meta {
    display: flex;
    gap: 1rem;
    color: #666;
    font-size: 0.9rem;
    margin-top: 0.5rem;
  }

  .post-content {
    line-height: 1.8;
    font-size: 1.1rem;
  }
</style>

一覧ページと同じ transition:name を使用することで、カードの画像が詳細ページのヒーロー画像へと滑らかに拡大・移動します。

以下の図は、記事一覧から詳細ページへの遷移フローです。

mermaidsequenceDiagram
  participant User as ユーザー
  participant List as 一覧ページ
  participant VT as View Transitions
  participant Detail as 詳細ページ

  User->>List: 記事カードをクリック
  List->>VT: transition:name で<br/>要素を識別
  VT->>VT: 画像とタイトルの<br/>位置・サイズを記録
  VT->>Detail: 新しいページを<br/>読み込み
  Detail->>VT: 同じ transition:name<br/>の要素を提供
  VT->>VT: アニメーション実行<br/>(拡大・移動)
  VT->>User: スムーズに詳細ページを表示

この仕組みにより、記事カードが詳細ページへと自然に変形します。

EC サイトでの商品詳細ページ遷移

EC サイトでは、商品一覧から詳細ページへの遷移を滑らかにすることで、ユーザーの購買体験を向上させることができます。

商品カードコンポーネント

astro---
// src/components/ProductCard.astro
interface Props {
  id: number;
  slug: string;
  name: string;
  image: string;
  price: number;
  rating: number;
}

const { id, slug, name, image, price, rating } = Astro.props;
---

<div class="product-card">
  <a href={`/products/${slug}`}>
    <!-- 商品画像 -->
    <div class="image-wrapper">
      <img
        src={image}
        alt={name}
        transition:name={`product-${id}`}
        class="product-image"
      />
    </div>

    <!-- 商品情報 -->
    <div class="product-info">
      <h3 class="product-name">{name}</h3>

      <div class="rating">
        {'★'.repeat(rating)}{'☆'.repeat(5 - rating)}
      </div>

      <p class="price">¥{price.toLocaleString()}</p>
    </div>
  </a>
</div>

<style>
  .product-card {
    background: white;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    transition: box-shadow 0.2s;
  }

  .product-card:hover {
    box-shadow: 0 4px 16px rgba(0,0,0,0.15);
  }

  .image-wrapper {
    aspect-ratio: 1;
    overflow: hidden;
    background: #f5f5f5;
  }

  .product-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .product-info {
    padding: 1rem;
  }

  .price {
    font-size: 1.25rem;
    font-weight: bold;
    color: #e63946;
  }
</style>

商品画像に transition:name を設定し、詳細ページへの遷移時に拡大アニメーションが適用されるようにしています。

商品詳細ページ

astro---
// src/pages/products/[slug].astro
import BaseLayout from '../../layouts/BaseLayout.astro';

const { slug } = Astro.params;

const product = {
  id: 1,
  name: 'ワイヤレスイヤホン Pro',
  image: '/images/product-1.jpg',
  price: 19800,
  rating: 4,
  description: '高音質なワイヤレスイヤホンです。',
  features: [
    'アクティブノイズキャンセリング',
    '最大30時間再生',
    'IPX4 防水'
  ]
};
---

<BaseLayout title={product.name}>
  <div class="product-detail">
    <div class="product-layout">
      <!-- 商品画像(一覧ページと同じ transition:name) -->
      <div class="image-section">
        <img
          src={product.image}
          alt={product.name}
          transition:name={`product-${product.id}`}
          class="main-image"
        />
      </div>

      <!-- 商品情報 -->
      <div class="info-section">
        <h1>{product.name}</h1>

        <div class="rating">
          {'★'.repeat(product.rating)}{'☆'.repeat(5 - product.rating)}
        </div>

        <p class="price">¥{product.price.toLocaleString()}</p>

        <p class="description">{product.description}</p>

        <ul class="features">
          {product.features.map((feature) => (
            <li>{feature}</li>
          ))}
        </ul>

        <button class="buy-button">カートに追加</button>
      </div>
    </div>
  </div>
</BaseLayout>

<style>
  .product-detail {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
  }

  .product-layout {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 3rem;
    align-items: start;
  }

  .main-image {
    width: 100%;
    border-radius: 8px;
  }

  .price {
    font-size: 2rem;
    font-weight: bold;
    color: #e63946;
    margin: 1rem 0;
  }

  .buy-button {
    background: #e63946;
    color: white;
    border: none;
    padding: 1rem 2rem;
    font-size: 1.1rem;
    border-radius: 8px;
    cursor: pointer;
    width: 100%;
    margin-top: 2rem;
  }

  @media (max-width: 768px) {
    .product-layout {
      grid-template-columns: 1fr;
    }
  }
</style>

一覧ページの小さい商品画像が、詳細ページの大きい画像へとスムーズに拡大するアニメーションが実現できました。

ポートフォリオサイトでの作品詳細表示

デザイナーや開発者のポートフォリオサイトでも、View Transitions は効果的です。

作品グリッドコンポーネント

astro---
// src/components/WorkGrid.astro
const works = [
  {
    id: 1,
    slug: 'website-redesign',
    title: 'Web サイトリニューアル',
    thumbnail: '/images/work-1.jpg',
    category: 'Web Design'
  },
  {
    id: 2,
    slug: 'mobile-app',
    title: 'モバイルアプリ UI',
    thumbnail: '/images/work-2.jpg',
    category: 'UI/UX'
  },
  // その他の作品...
];
---

<div class="work-grid">
  {works.map((work) => (
    <a href={`/works/${work.slug}`} class="work-item">
      <img
        src={work.thumbnail}
        alt={work.title}
        transition:name={`work-image-${work.id}`}
        class="work-thumbnail"
      />

      <div class="work-overlay">
        <span class="category">{work.category}</span>
        <h3 transition:name={`work-title-${work.id}`}>
          {work.title}
        </h3>
      </div>
    </a>
  ))}
</div>

<style>
  .work-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 1.5rem;
  }

  .work-item {
    position: relative;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    border-radius: 8px;
    display: block;
  }

  .work-thumbnail {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.3s;
  }

  .work-item:hover .work-thumbnail {
    transform: scale(1.05);
  }

  .work-overlay {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 1.5rem;
    background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
    color: white;
  }

  .category {
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
  }
</style>

作品のサムネイル画像とタイトルに transition:name を設定しています。

作品詳細ページ

astro---
// src/pages/works/[slug].astro
import BaseLayout from '../../layouts/BaseLayout.astro';

const { slug } = Astro.params;

const work = {
  id: 1,
  title: 'Web サイトリニューアル',
  thumbnail: '/images/work-1.jpg',
  category: 'Web Design',
  client: '株式会社サンプル',
  year: 2025,
  description: 'クライアントの要望に応じて、Web サイトを全面的にリニューアルしました。',
  images: [
    '/images/work-1-detail-1.jpg',
    '/images/work-1-detail-2.jpg',
    '/images/work-1-detail-3.jpg'
  ]
};
---

<BaseLayout title={work.title}>
  <article class="work-detail">
    <!-- メインビジュアル -->
    <div class="hero">
      <img
        src={work.thumbnail}
        alt={work.title}
        transition:name={`work-image-${work.id}`}
        class="hero-image"
      />
    </div>

    <!-- 作品情報 -->
    <div class="work-content">
      <header class="work-header">
        <span class="category">{work.category}</span>
        <h1 transition:name={`work-title-${work.id}`}>
          {work.title}
        </h1>
      </header>

      <div class="work-meta">
        <div class="meta-item">
          <strong>クライアント:</strong> {work.client}
        </div>
        <div class="meta-item">
          <strong>年度:</strong> {work.year}
        </div>
      </div>

      <p class="description">{work.description}</p>

      <!-- 追加の画像 -->
      <div class="image-gallery">
        {work.images.map((img) => (
          <img src={img} alt="作品詳細" />
        ))}
      </div>
    </div>
  </article>
</BaseLayout>

<style>
  .hero {
    width: 100%;
    height: 60vh;
    overflow: hidden;
  }

  .hero-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .work-content {
    max-width: 800px;
    margin: 0 auto;
    padding: 3rem 2rem;
  }

  .work-meta {
    display: flex;
    gap: 2rem;
    margin: 1.5rem 0;
    padding: 1.5rem;
    background: #f5f5f5;
    border-radius: 8px;
  }

  .image-gallery {
    display: grid;
    gap: 2rem;
    margin-top: 3rem;
  }

  .image-gallery img {
    width: 100%;
    border-radius: 8px;
  }
</style>

この実装により、作品グリッドのサムネイルが詳細ページのヒーロー画像へと美しく拡大します。

パフォーマンス最適化のテクニック

View Transitions を使用する際、パフォーマンスにも注意を払う必要があります。

画像の遅延読み込み

大量の画像を扱う場合、遅延読み込みを実装します。

astro---
// src/components/LazyImage.astro
interface Props {
  src: string;
  alt: string;
  transitionName?: string;
}

const { src, alt, transitionName } = Astro.props;
---

<img
  src={src}
  alt={alt}
  loading="lazy"
  decoding="async"
  transition:name={transitionName}
  class="lazy-image"
/>

<style>
  .lazy-image {
    background: #f0f0f0;
  }
</style>

loading="lazy" 属性により、画像は画面に表示される直前に読み込まれます。

プリフェッチの活用

ユーザーがリンクにホバーした際に、次のページを事前に読み込むことで、遷移をさらに高速化できます。

astro---
// src/layouts/BaseLayout.astro
import { ClientRouter } from 'astro:transitions';
---

<html lang="ja">
  <head>
    <!-- プリフェッチを有効化 -->
    <ClientRouter prefetch />
  </head>
  <body>
    <slot />
  </body>
</html>

prefetch 属性を追加するだけで、リンクホバー時の自動プリフェッチが有効になります。

以下の図は、プリフェッチの動作フローです。

mermaidflowchart TD
  hover["ユーザーがリンクに<br/>ホバー"]
  hover --> fetch["バックグラウンドで<br/>次ページを取得"]
  fetch --> cache["ブラウザキャッシュに保存"]
  cache --> waitNode["ユーザーのクリックを待機"]
  waitNode --> clickEvt["クリック"]
  clickEvt --> instant["キャッシュから即座に表示"]
  instant --> transition["View Transition<br/>アニメーション実行"]


プリフェッチにより、クリック時の待機時間がほぼゼロになります。

まとめ

Astro の View Transitions は、シンプルな設定で SPA 並みの滑らかなページ遷移を実現できる強力な機能です。この記事で紹介した内容をまとめます。

View Transitions の主なメリット

View Transitions を導入することで、以下のメリットが得られます。

#メリット説明
1簡単な導入<ClientRouter ​/​> を追加するだけで有効化
2軽量JavaScript フレームワークに比べて圧倒的に軽量
3柔軟性ビルトインとカスタムアニメーションの両方に対応
4ブラウザ互換性フォールバック機能により幅広いブラウザで動作
5アクセシビリティprefers-reduced-motion など標準対応

実装のポイント

実装時には、以下のポイントを押さえておきましょう。

基本設定: 共通レイアウトの <head><ClientRouter ​/​> を配置するだけで、サイト全体に View Transitions が適用されます。

要素の対応付け: transition:name ディレクティブを使って、異なるページ間で同じ要素を対応付けることで、要素が滑らかに変形・移動するアニメーションを実現できます。

状態の保持: transition:persist を使えば、動画やオーディオプレーヤーなどの状態をページ遷移後も維持できます。

カスタマイズ: CSS の ::view-transition 疑似要素を使って、独自のアニメーションを定義できます。

フォールバック: 未対応ブラウザ向けに適切なフォールバック戦略を用意することで、すべてのユーザーに快適な体験を提供できます。

パフォーマンスとアクセシビリティ

View Transitions を実装する際は、パフォーマンスとアクセシビリティにも配慮が必要です。

画像の遅延読み込みやプリフェッチを活用することで、さらに高速な遷移が実現できます。また、prefers-reduced-motion メディアクエリに対応することで、アニメーションに敏感なユーザーにも配慮した実装が可能です。

適用シーンの多様性

ブログ、EC サイト、ポートフォリオなど、さまざまな種類の Web サイトで View Transitions は効果を発揮します。特に、画像を多用するサイトや、ページ間で要素の連続性を保ちたいサイトには最適です。

従来の静的サイトの高速性と、SPA の滑らかな UX を両立できる Astro の View Transitions は、これからの Web 開発において重要な技術となるでしょう。

ぜひこの記事を参考に、あなたのプロジェクトでも View Transitions を導入して、ユーザー体験を向上させてみてください。

関連リンク