Communitygithub.com

joo6077/react-animation

React プロジェクトでアニメーションを実装するためのスキルです。ライブラリを一切使わず、Tailwind CSS、View Transitions API、Pointer Events のみを用いて、フェードイン、ドラッグ&ドロップ、ページ遷移などのアニメーションを段階的に実装します。

react-animation とは?

このスキルは、React アプリケーションにおけるアニメーション実装を支援するための包括的なガイドです。最大の特徴は、すべてのアニメーションを外部ライブラリに依存せず、Tailwind CSS、CSS @keyframes、View Transitions API、そして生の Pointer Events と状態機械だけで実装する点です。Tier 1(簡単なトランジション/ホバー効果)、Tier 2(ビュー遷移/FLIPアニメーション)、Tier 3(ドラッグ&ドロップ/ジェスチャー)の3段階に分かれており、ユーザーの要求に応じて最も低いTierから適用します。各Tierごとに具体的なコード例と注意点(Gotchas)が豊富に含まれており、特にパフォーマンス、アクセシビリティ(`prefers-reduced-motion`)、ブラウザ互換性に細かく配慮しています。Claude Code や Cursor などの AI エージェントで使用することを想定して設計されており、自動的なTier判定のためのキーワード表や、アニメーションの動作確認を徹底するための検証プロトコル(`render-evidence-protocol`)への言及もあります。Firefox や Safari の対応状況に関する最新の情報(2026年時点)も反映されており、プロダクション品質のアニメーションを安全に実装するための信頼できるリファレンスと言えます。

対応✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/joo6077/claude-plugins/tree/HEAD/react-kit/skills/react-animation

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

React Animation

インストール版プラグインには docs/react/ がありません — このファイルの docs/... パスを開けない場合は https://raw.githubusercontent.com/joo6077/claude-plugins/main/ の後ろに同じパスを付けて読み、それでも読めない場合は内容を捏造せずに読めなかったと記載します。

Gotchas

  1. ライブラリ0個の原則 — 絶対に例外なし: Motion (framer-motion) / dnd-kit / react-spring / react-transition-group / @formkit/auto-animate / react-dnd / gsap / lottie-react / react-beautiful-dnd / animate.css はインストールおよび import 禁止です。これらの import 文がコードベースに存在すると /react-audit がビルド失敗を発行します。ユーザーが要求しても代替実装を提案し、ライブラリの使用を拒否します。

  2. 最も低い Tier から試行: 「animation」リクエストを受け取ったら、無条件にTier 3から使うミスが頻発します。判定ルールに従い、まず Tier 1 で解決可能か確認し、不可能な場合のみ上位のTierに進みます。

  3. tailwindcss-animate プラグインの確認: animate-in、fade-in-*、slide-in-from-* のような拡張ユーティリティは tailwindcss-animate が必要です。shadcn をインストールしたプロジェクトにはデフォルトで含まれますが、ない場合は pnpm add -D tailwindcss-animate 後、tailwind.config.ts にプラグイン登録を案内します。

  4. view-transition-name の一意性: 同じ名前を2つの要素が同時に持つと「duplicate name」エラーが発生します。必ず動的 ID (view-transition-name: task-${item.id}) を使用して一意性を保証します。

  5. Pointer cancel 処理必須: pointercancel イベントを処理しないと、タブ切り替え、他のアプリへのフォーカス、要素の unmount 時にドラッグ状態がリークします。onPointerCancel ハンドラーで必ず FSM を idle にリセットします。

  6. touch-action: none を忘れるとモバイルドラッグ不可: pointer event ベースのドラッグ要素に Tailwind の touch-none クラスを必ず追加し、ブラウザのデフォルトスクロール/ズームをブロックします。

  7. Zustand drag store はグローバル単一インスタンス: 機能ごとに個別の drag store を作成しません。src/presentation/shared/stores/drag-store.ts の1つをすべての機能が共有します。

  8. prefers-reduced-motion 未適用: すべてのTierで @media (prefers-reduced-motion: reduce) または Tailwind の motion-reduce:* バリアントを使用してアニメーションを無効化する必要があります。/react-audit が未適用時に警告を発行します。

  9. アニメーションは presentation 専用: domain/、data/ レイヤーに pointer event / view transition / Zustand drag store を import しません。アニメーション関連コードは src/presentation/ にのみ配置します。

  10. Firefox の types パラメータ未サポート: document.startViewTransition({ update, types: [...] }) の types オプションは2026年時点で Chromium 系でのみ安定してサポートされています。withViewTransition ラッパーで try/catch によるフォールバックを処理します。

  11. scroll-driven animation のブラウザサポート (2026-08-13 更新): animation-timeline: scroll() / scroll-timeline は Can I Use 基準グローバル 85.43% — Chrome/Edge 115+、Safari/iOS 26+、Firefox 156+ です。Firefox がフラグの背後にあるという記述は古いです。 ただし、MDN 基準では view() はまだ not Baseline であるため、@supports (animation-timeline: scroll()) / @supports (animation-timeline: view()) でラップし、非サポート時は静的スタイルでフォールバックする方針はそのまま維持します。

  12. view-transition-name: match-element の活用 (Chrome 137+): 数十の要素に手動で一意な名前を付ける代わりに、view-transition-name: match-element CSS 値を使用するとブラウザが自動的に要素をマッチングします。ただし、2026-Q2 時点では Chromium 専用のため、フォールバックパスを維持します。

  13. モーションは静的コードでは証明されない — 完了宣言前の証拠必須 (E2): このスキルの成果物は再生されて初めて存在を確認できます。animate-* クラスが付いている事実、コードが startViewTransition を呼んでいる事実は R3(静的) 証拠であり、アニメーションが実際に再生されたという証拠ではありません — keyframe 未定義、prefers-reduced-motion 常時ヒット、親の overflow: hidden クリッピング、非サポートブラウザのフォールバック突入は、すべて静かに「何も起こらない」で終わります。完了直前に react-kit/references/render-evidence-protocol.md §4 のチェックリストを埋めます。スナップショット比較で検証する場合は、前/後の2時点を指定します — 静止フレーム1枚はどんなモーションでも同じ結果を返すため oracle ではありません (§3 d)。証拠が得られない場合は [未検証] を付け、4つの欄 (妨げたもの · 試みた回避策 · 制御不能な理由 · 再検証命令 — 規約 §2) を埋めて部分完了として報告します。

    証拠確保のためにアニメーションライブラリを導入しません。 Library Policy (Gotcha #1) はこの規約よりも上位であり、いかなる検証の便宜も緩和されません。

  14. 基準キャプチャは編集前に撮る — react-kit/references/render-evidence-protocol.md §1 Step 0 と §2 比較反復順序の1番を最初の編集前に実行し、その結果 (言い換え · 慣例表 · 現在の姿のキャプチャパスと変更されるべき標識) を応答に残します。編集後は編集前の姿を再び撮ることはできません。

  15. Tier 2 は更新の種類で区別する — React 19.3 の <ViewTransition> は安定API です (2026-09-26 追加): react からインポートする <ViewTransition> は 19.3 で実験APIではなく安定APIになりました。ラップされた子が Transition としてマークされた更新 (startTransition 内の更新 · Suspense reveal · useDeferredValue) でスタイルが変更されるか、マウント・アンマウントされるときに動作し、DOM でのみ動作します (出典: https://react.dev/blog/2026/09/09/react-19-3 「We shared it as an experimental API last year, and in 19.3 it's stable and ready to use.」)。したがって、Transition としてマークされた React 状態更新には <ViewTransition> を使用し、それ以外の DOM 更新 (Transition ではない同期更新 · React 外の DOM 操作) には §3.1 の withViewTransition ラッパーをそのまま使用します。ラッパーは削除しません。react パッケージ内のコンポーネントであるため、ライブラリ0個の原則 (#1) には抵触しません。プロジェクトの react バージョンが 19.3 未満の場合はラッパーのみを使用します。

プロセス

1. 自動ティア判定

--tier フラグがない場合、ユーザーリクエストを分析して最も低いティアを選択します。

キーワード / シナリオ判定 Tier
「fade in」「slide up」「scale」「pulse」「bounce」「hover 効果」「opacity」「shimmer」「エントリーアニメーション」T1
「skeleton ローディング → 完了遷移」「ボタン hover」「accordion」「モーダル open/close」「状態変化」T1
「スクロールアニメーション」「scroll-driven」「parallax」「スクロール進行バー」「スクロールベース」T1
「ページ遷移」「shared element」「grid to board」「ビュー遷移」「ルート遷移」「DOM構造変更」T2
「scroll-triggered」「スクロール交差時トリガー」「特定位置からアニメーション開始」T2
「ドラッグアンドドロップ」「並び替えリスト」「sortable」「kanban」「ドラッグ」「ドロップ」「gesture」「矢印接続線」T3

T2/T3 の境界が曖昧な場合は animation-architect-react エージェントに相談します。

3-Tier 概要:

Tierツール適用シナリオ難易度
T1Tailwind animate-* + CSS @keyframes + scroll-driven状態変化、hover、単純ループ、スクロール連動低
T2View Transitions API (withViewTransition ラッパー) · React 19.3 <ViewTransition> (Transition としてマークされた更新のみ — Gotcha 15)ビュー/ルート遷移、shared element、FLIP中
T3Pointer Events + FSM + requestAnimationFrameドラッグアンドドロップ、ジェスチャー、SVG 接続線高

2. Tier 1 — Tailwind + CSS 実装

2.1 Tailwind 組み込みユーティリティ

カテゴリユーティリティ例
Transitiontransition-transform、transition-colors、transition-opacityhover:scale-105 transition-transform duration-200
Durationduration-75~duration-1000duration-300
Easingease-linear、ease-in、ease-out、ease-in-outease-in-out
Animation (組み込み)animate-spin、animate-ping、animate-pulse、animate-bounceローディング、通知
Delaydelay-75~delay-1000stagger 効果

2.2 tailwindcss-animate 拡張ユーティリティ

tailwindcss-animate プラグインが提供する enter/exit アニメーション:

  • animate-in / animate-out
  • fade-in-*、fade-out-*
  • slide-in-from-top/bottom/left/right-*、slide-out-to-*
  • zoom-in-*、zoom-out-*

data-[state=...] 属性セレクターと組み合わせて状態ベースのアニメーションを実装します:

// ドロワー open/close の例
<div
  data-state={open ? 'open' : 'closed'}
  className="
    data-[state=open]:animate-in data-[state=closed]:animate-out
    data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
    data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left
    duration-300
    motion-reduce:transition-none motion-reduce:animate-none
  "
>
  ドロワー内容
</div>

2.3 CSS @keyframes の直接宣言

Tailwind ユーティリティでカバーできないカスタムアニメーションは globals.css の @theme ブロックに宣言します:

/* src/presentation/styles/globals.css */
@import "tailwindcss";

@theme {
  --animate-wiggle: wiggle 1s ease-in-out infinite;
  --animate-shimmer: shimmer 2s linear infinite;

  @keyframes wiggle {
    0%, 100% { transform: rotate(-3deg); }
    50%      { transform: rotate(3deg); }
  }

  @keyframes shimmer {
    from { background-position: -200% 0; }
    to   { background-position: 200% 0; }
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

使用: <div className="animate-wiggle">...</div>

2.4 Tier 1 シナリオ例

Fade-in エントリー:

<div className="animate-in fade-in-0 duration-500 motion-reduce:animate-none">
  ページロード時にフェードイン
</div>

Hover scale ボタン:

<button className="transition-transform duration-200 hover:scale-105 active:scale-95 motion-reduce:transition-none">
  ボタン
</button>

スタッガーリストエントリー:

{items.map((item, i) => (
  <div
    key={item.id}
    className="animate-in slide-in-from-bottom-4 fade-in-0 motion-reduce:animate-none"
    style={{ animationDelay: `${i * 50}ms`, animationFillMode: 'backwards' }}
  >
    {item.name}
  </div>
))}

2.5 Scroll-driven Animations (CSS ネイティブ)

スクロール位置に連動するアニメーションを JS なしで CSS animation-timeline で実装します。2026-08-13 時点で scroll() / scroll-timeline は Chrome/Edge 115+、Safari/iOS 26+、Firefox 156+ でサポートされており (Can I Use グローバル 85.43%)、view() は MDN 基準でまだ not Baseline です。どちらの場合も @supports でラップして非サポートブラウザで graceful fallback します。

スクロール進行バー:

/* globals.css */
@keyframes progress-grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
<div
  className="fixed top-0 left-0 h-1 w-full origin-left bg-primary motion-reduce:hidden"
  style={{
    animation: 'progress-grow linear',
    animationTimeline: 'scroll(root block)',
  }}
/>

要素エントリー時の fade-in (view timeline):

@keyframes fade-slide-in {
  from { opacity: 0; transform: translateY(2rem); }
  to   { opacity: 1; transform: translateY(0); }
}

@supports (animation-timeline: view()) {
  .scroll-reveal {
    animation: fade-slide-in linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 100%;
  }
}
<section className="scroll-reveal motion-reduce:opacity-100">
  スクロール時に出現するコンテンツ
</section>

注意: scroll-triggered animations (Chrome 145, 2026) は、特定のスクロールオフセット交差時に時間ベースのアニメーションをトリガーする別個の概念です。scroll-driven と混同しないでください。scroll-triggered は Tier 2 の候補として分類します。

3. Tier 2 — View Transitions API 実装

same-document View Transitions は MDN 基準 Baseline 2025、Can I Use 基準グローバル 90.2% — Chrome/Edge 111+、Safari 18+、Firefox 144+ (2026-08-13 更新)。SPA 内の DOM 変更時に自動 FLIP アニメーション。

一方、@view-transition ベースの cross-document / MPA 遷移といくつかの詳細機能はまだ limited availability です。ルート遷移に使用する場合は same-document パスをデフォルトとし、feature detection + fallback を維持します (§6 標準カバレッジギャップ番号8)。

3.1 withViewTransition ラッパー作成

ファイルがない場合は作成します:

// src/presentation/shared/lib/view-transition.ts
export function withViewTransition(
  updateDOM: () => void,
  options?: { types?: string[] },
): void {
  // prefers-reduced-motion 尊重
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    updateDOM()
    return
  }
  // Feature detection (旧ブラウザ fallback)
  if (typeof document.startViewTransition !== 'function') {
    updateDOM()
    return
  }
  // types パラメータサポート有無の分岐 (Firefox 初期実装は非サポート)
  try {
    if (options?.types && options.types.length > 0) {
      document.startViewTransition({ update: updateDOM, types: options.types })
    } else {
      document.startViewTransition(updateDOM)
    }
  } catch {
    updateDOM()
  }
}

3.2 view-transition-name CSS 共有要素

DOM 構造が変わっても「同じ要素」として識別し、スムーズに移動させます:

// 動的 ID で一意性を保証 (重複名禁止)
<div
  className="rounded-lg border p-4"
  style={{ viewTransitionName: `task-${task.id}` }}
>
  {task.title}
</div>

3.3 グリッド ↔ ボードビュー遷移 (代表シナリオ)

// src/presentation/features/tasks/components/task-switcher.tsx
import { withViewTransition } from '@/presentation/shared/lib/view-transition'
import { useTaskStore } from '../store'

export function TaskViewSwitcher() {
  const viewMode = useTaskStore((s) => s.viewMode)
  const setViewMode = useTaskStore((s) => s.setViewMode)

  return (
    <button
      onClick={() => {
        withViewTransition(() => {
          setViewMode(viewMode === 'grid' ? 'board' : 'grid')
        })
      }}
    >
      {viewMode === 'grid' ? 'ボードビューへ' : 'グリッドビューへ'}
    </button>
  )
}

export function TaskList({ tasks }: { tasks: Task[] }) {
  const viewMode = useTaskStore((s) => s.viewMode)
  return (
    <div className={viewMode === 'grid' ? 'grid grid-cols-3 gap-4' : 'flex gap-4 overflow-x-auto'}>
      {tasks.map((task) => (
        <div
          key={task.id}
          className="rounded-lg border p-4"
          style={{ viewTransitionName: `task-${task.id}` }}
        >
          {task.title}
        </div>
      ))}
    </div>
  )
}

3.4 CSS 遷移の詳細調整

/* globals.css — 遷移 duration + easing */
::view-transition-group(*) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* 特定要素の個別調整 */
::view-transition-group(hero-image) {
  animation-duration: 0.6s;
}

/* reduced-motion で即時遷移 */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*) {
    animation-duration: 0.01ms;
  }
}

4. Tier 3 — カスタム Pointer Primitives 実装

ライブラリなしで Pointer Events API + 状態機械(FSM) で直接実装します。

4.1 useDrag フック作成

// src/presentation/shared/hooks/use-drag.ts
import { useCallback, useEffect, useReducer, useRef } from 'react'

// FSM 3状態: idle / dragging / dropping
type DragState =
  | { kind: 'idle' }
  | { kind: 'dragging'; startX: number; startY: number; offsetX: number; offsetY: number; pointerId: number }
  | { kind: 'dropping'; offsetX: number; offsetY: number }

type DragAction =
  | { type: 'start'; x: number; y: number; pointerId: number }
  | { type: 'move'; x: number; y: number }
  | { type: 'drop' }
  | { type: 'resolved' }
  | { type: 'cancel' }

function dragReducer(state: DragState, action: DragAction): DragState {
  switch (action.type) {
    case 'start':
      return { kind: 'dragging', startX: action.x, startY: action.y, offsetX: 0, offsetY: 0, pointerId: action.pointerId }
    case 'move':
      if (state.kind !== 'dragging') return state
      return { ...state, offsetX: action.x - state.startX, offsetY: action.y - state.startY }
    case 'drop':
      if (state.kind !== 'dragging') return state
      return { kind: 'dropping', offsetX: state.offsetX, offsetY: state.offsetY }
    case 'resolved':
    case 'cancel':
      return { kind: 'idle' }
  }
}

type DragHandlers = {
  onPointerDown: (e: React.PointerEvent) => void
  onPointerMove: (e: React.PointerEvent) => void
  onPointerUp: (e: React.PointerEvent) => void
  onPointerCancel: (e: React.PointerEvent) => void
}

export function useDrag(): {
  state: DragState
  handlers: DragHandlers
  ref: React.RefObject<HTMLElement | null>
  resolve: () => void
} {
  const [state, dispatch] = useReducer(dragR

... (以降は省略)

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

関連スキル