アニメーション付き ライト/ダーク テーマ切替

View Transitions API でいまや定番となった「サークルワイプ」テーマ切替エフェクトを実現。スナップショット擬似要素に clip-path を当てる創造的な使い方。

UI Patterns

詳細な説明

サークルワイプエフェクト

Chrome の DevRel デモで有名になったこのエフェクトは、クリック位置から円形の reveal を成長させ、ライトからダークへページ全体をきれいなワイプで遷移させます。

async function toggleTheme(event) {
  const x = event.clientX;
  const y = event.clientY;

  const transition = document.startViewTransition(() => {
    document.documentElement.classList.toggle('dark');
  });

  await transition.ready;

  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y),
  );

  document.documentElement.animate(
    {
      clipPath: [
        `circle(0px at ${x}px ${y}px)`,
        `circle(${endRadius}px at ${x}px ${y}px)`,
      ],
    },
    {
      duration: 500,
      easing: 'ease-in-out',
      pseudoElement: '::view-transition-new(root)',
    },
  );
}

仕組み

  1. startViewTransition がライトテーマをキャプチャ。
  2. クラストグルでダークへ切替、React 再描画。
  3. ブラウザがダークテーマをキャプチャ。
  4. await transition.ready でスナップショット準備完了を待機。
  5. ::view-transition-new(root) 擬似要素の clip-path をアニメーション。新しい(ダーク)スナップショットを成長する円で reveal、その下に古い(ライト)スナップショットが表示され続けます。

方向反転

ダーク→ライトでは、新しい擬似要素を成長させる代わりに 古い 擬似要素を縮小アニメーションさせます。あるいは常に new を成長させてもよく、ユーザーの知覚はどちらの方向でも同じなので両方向で同様に機能します。

Reduced motion

if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
  document.documentElement.classList.toggle('dark');
  return;
}

このプリファレンスを設定したユーザーにはアニメーションを完全にスキップします。

ユースケース

ブランドの一部としての視覚的演出を持つマーケティングサイトやダッシュボードのテーマスイッチャー。実例: Chrome 111+ DevRel デモ、複数の CSS Day トーク、Linear の設定パネルなど。

試してみるView Transitions API ジェネレーター

フルツールを開く