オプション

motion-foundations

affaan-m/ECC affaan-m/ECC

motion/react を使用した React / Next.js 向けのモーショントークン、スプリングプリセット、パフォーマンスルール、デバイス適応、アクセシビリティの徹底、および SSR の安全性。基盤層 — その他のすべてのモーション関連スキルは、これに依存しています。

...すべて拡張します
51
更新された時間 2026年7月29日

モーションの基礎

モーションシステムの基盤となる層です。下流のスキル(motion-patterns, motion-advanced)が継承するすべての値、制約、および ルールを定義します。 アニメーション作業を開始する前に、このスキルをロードしてください。

有効化のタイミング

  • アニメーション付きコンポーネントをゼロから作成する場合
  • トークン、スプリングプリセット、またはイージング値の設定時
  • 実装 prefers-reduced-motion サポート
  • アニメーションの初期状態におけるハイドレーションの不一致のデバッグ
  • アニメーションをそもそも実装すべきかどうかの検討

成果物

このスキルは以下を生成します:

  • 共有 motionTokens オブジェクト(持続時間、イージング、距離、スケール)
  • 共有 springs プリセットマップ(5つの名前付き設定)
  • すべてのコンポーネントで共有される shouldAnimate() すべてのコンポーネントで共有されるゲート
  • アクセシビリティに準拠したアニメーションのデフォルト設定(以下を通じて) useReducedMotion
  • ハイドレーション警告がゼロのSSR対応初期状態

原則

モーションは、以下のいずれかを満たすか、さもなければ削除されなければなりません:

  • 注意を誘導する
  • 状態を伝える
  • 空間的な連続性を維持する

応答性は常に滑らかさよりも優先される。入力遅延を引き起こす60 fpsのアニメーションは、 アニメーションがない場合よりも劣る。

ルール

これらは絶対条件です。システム内のすべてのコンポーネントに適用されます。

  1. motion/reactのみを使用すること。 framer-motionからインポートしてはいけません。同じツリー内でこれらを混在させてはいけません。
  2. initial はサーバーの出力と一致しなければなりません。サーバーが opacity: 1場合、 initial propも opacity: 1でなければなりません。例外は認められません。
  3. モーションの低減はすべてに優先します。 useReducedMotion() が true または prefersReduced が true状態になると、すべてのトランスフォームが無効化されます。不透明度のみによる、0.2秒以下のフェードのみが許可される代替手段です。
  4. レイアウトプロパティをアニメーション化してはなりません。 width, height, top, left, margin, padding は animate。代わりに transform および opacity のみを使用してください。
  5. すべてのトークン値は `motionTokens` から取得されます。コンポーネントファイル内での持続時間やイージングのハードコーディングは禁止されています。
  6. すべてのスプリング設定は、springsマップから取得されます。インラインの stiffness/damping 値の使用は禁止されています。
  7. "use client" は、からインポートするすべてのファイルで必須です motion/react.
  8. モジュールレベルでwindowやnavigatorを読み取ってはなりません。常に typeof window !== "undefined".

決定の指針

期間の選択

スプリングの選択

アニメーションを完全に無効にするタイミング

無効化( shouldAnimate() return false)を無効にするタイミング:

  • prefersReduced が true
  • isLowEnd 以下の場合 true かつ、アニメーションが必須でない場合
  • 要素が画面外にあり、ビューポートに入ることはない場合
  • アニメーションが純粋に装飾的なものであり、UX上の目的がない場合

基本概念

トークンシステム

// lib/motion-tokens.ts
export const motionTokens = {
  duration: {
    instant: 0.08,
    fast:    0.18,
    normal:  0.35,
    slow:    0.6,
    crawl:   1.0,
  },
  easing: {
    smooth: [0.22, 1, 0.36, 1],
    sharp:  [0.4, 0, 0.2, 1],
    bounce: [0.34, 1.56, 0.64, 1],
    linear: [0, 0, 1, 1],
  },
  distance: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 48,
  },
  scale: {
    subtle: 0.98,
    press:  0.95,
    pop:    1.04,
  },
}

export const springs = {
  snappy:  { type: "spring", stiffness: 300, damping: 30 },
  gentle:  { type: "spring", stiffness: 120, damping: 14 },
  bouncy:  { type: "spring", stiffness: 400, damping: 10 },
  instant: { type: "spring", stiffness: 600, damping: 35 },
  release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
}

ランタイムフラグ

// lib/motion-config.ts
export const motionConfig = {
  isLowEnd() {
    return (
      typeof navigator !== "undefined" &&
      navigator.hardwareConcurrency <= 4
    )
  },

  prefersReduced() {
    return (
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    )
  },

  shouldAnimate({ essential = false } = {}) {
    if (this.prefersReduced()) return false
    if (!essential && this.isLowEnd()) return false
    return true
  },

  duration() {
    return this.isLowEnd() || this.prefersReduced()
      ? motionTokens.duration.instant
      : motionTokens.duration.normal
  },
}

アクセシビリティ

優先順位(高い順):

  1. prefers-reduced-motion: reduce — すべてのトランスフォームを無効化し、不透明度の遷移時間を0.2秒以下に制限する
  2. 低スペック端末の検出 — アニメーションの持続時間を短縮し、不要なアニメーションを削除
  3. デザイン設定 — それ以外すべて

モーションは滑らかに劣化する必要があります。レイアウトのずれを引き起こしたり、 方向感覚を混乱させたりするような、突然の消失は決してあってはなりません。

// hooks/use-reduced-motion.tsx
"use client"
import { useReducedMotion } from "motion/react"

export function useSafeMotion(fullY: number = 16) {
  const reduce = useReducedMotion()
  return {
    initial: { opacity: 0, y: reduce ? 0 : fullY },
    animate: { opacity: 1, y: 0 },
    exit:    { opacity: 0, y: reduce ? 0 : -fullY },
  }
}
/* globals.css */
@media (prefers-reduced-motion: reduce) {
  .motion-safe-transition  { transition: opacity 0.15s; }
  .motion-reduce-transform { transform: none !important; }
}

<div class="motion-safe:animate-fade motion-reduce:opacity-100">div>

SSR / ハイドレーションの安全性

ルール:initialは常にサーバーがレンダリングした内容と一致しなければならない。

// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />

// CORRECT — use AnimatePresence or defer to client mount
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])

div
  initial={{ opacity: mounted ? 0 : 1 }}
  animate={{ opacity: 1 }}
/>

コード例

エンドツーエンド:トークン + スプリング + アクセシビリティ + SSR ガード

// components/fade-in-card.tsx
"use client"

import { useState, useEffect } from "react"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"
import { motionConfig } from "@/lib/motion-config"

interface FadeInCardProps {
  children: React.ReactNode
  delay?: number
}

export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
  // SSR guard — initial must match server output (opacity: 1)
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])

  // Accessibility — disables transform when reduced motion is preferred
  const safeMotion = useSafeMotion(motionTokens.distance.md)

  // Device gate — skip animation on low-end hardware
  if (!motionConfig.shouldAnimate() || !mounted) {
    return <div>{children}div>
  }

  return (
    <motion.div
      initial={safeMotion.initial}
      animate={safeMotion.animate}
      exit={safeMotion.exit}
      transition={{
        ...springs.gentle,
        delay,
      }}
      whileHover={{ scale: motionTokens.scale.pop }}
      whileTap={{ scale: motionTokens.scale.press }}
    >
      {children}
    motion.div>
  )
}

制約/対象外

このスキルでは以下の内容は扱いません:

  • UIコンポーネントのパターン(ボタン、モーダル、スタッガー) → 参照 motion-patterns
  • ドラッグ、ジェスチャー、SVG、テキストアニメーション、カスタムフック → 参照 motion-advanced
  • CSSのみのアニメーションやTailwindの animate-* クラスを使用しない motion/react
  • サードパーティ製アニメーションライブラリ(GSAP、anime.jsなど)を使用しないCSSのみのアニメーション
  • モーションデザインの決定(いつアニメーションさせるか、何を強調するか) — これはデザインの課題であり、コード上の制約ではない

アンチパターン

関連スキル

  • motion-patterns — ここで定義されたトークンとスプリングを使用して、ボタン、モーダル、スタッガー、ページ遷移、スクロールのパターンを構築します。いかなる値も再定義しません。
  • motion-advanced — ここで定義されたトークンとスプリングを使用して、ドラッグ、SVG、テキスト、ジェスチャーのパターンを生成します。この基盤の上に useAnimate シーケンスやカスタムフックを追加します。
GitHubで見る

Motion Foundations

The base layer of the motion system. Defines every value, constraint, and rule that downstream skills (motion-patterns, motion-advanced) inherit. Load this skill before any animation work begins.

When to Activate

  • Starting any animated component from scratch
  • Setting up tokens, spring presets, or easing values
  • Implementing prefers-reduced-motion support
  • Debugging hydration mismatches from animation initial states
  • Evaluating whether an animation should exist at all

Outputs

This skill produces:

  • A shared motionTokens object (duration, easing, distance, scale)
  • A shared springs preset map (5 named configs)
  • A shouldAnimate() gate used by all components
  • Accessibility-compliant animation defaults via useReducedMotion
  • SSR-safe initial states with zero hydration warnings

Principles

Motion must do at least one of the following or it must be removed:

  • Guide attention
  • Communicate state
  • Preserve spatial continuity

Responsiveness always outranks smoothness. A 60 fps animation that causes input delay is worse than no animation.

Rules

These are non-negotiable. They apply to every component in the system.

  1. Use motion/react only. Never import from framer-motion. Never mix the two in the same tree.
  2. initial must match server output. If the server renders opacity: 1, the initial prop must also be opacity: 1. No exceptions.
  3. Reduced motion overrides everything. When useReducedMotion() returns true or prefersReduced is true, all transforms are disabled. Opacity-only fades at ≤ 0.2s are the only permitted fallback.
  4. Never animate layout properties. width, height, top, left, margin, padding are banned from animate. Use transform and opacity only.
  5. All token values come from motionTokens. Hardcoded durations and easings in component files are forbidden.
  6. All spring configs come from the springs map. Inline stiffness/damping values are forbidden.
  7. "use client" is required on every file that imports from motion/react.
  8. Never read window or navigator at module level. Always guard with typeof window !== "undefined".

Decision Guidance

Choosing a duration

Choosing a spring

When to disable animation entirely

Disable (make shouldAnimate() return false) when:

  • prefersReduced is true
  • isLowEnd is true and the animation is non-essential
  • The element is off-screen and will never enter the viewport
  • The animation is purely decorative with no UX purpose

Core Concepts

Token system

// lib/motion-tokens.ts
export const motionTokens = {
  duration: {
    instant: 0.08,
    fast:    0.18,
    normal:  0.35,
    slow:    0.6,
    crawl:   1.0,
  },
  easing: {
    smooth: [0.22, 1, 0.36, 1],
    sharp:  [0.4, 0, 0.2, 1],
    bounce: [0.34, 1.56, 0.64, 1],
    linear: [0, 0, 1, 1],
  },
  distance: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 48,
  },
  scale: {
    subtle: 0.98,
    press:  0.95,
    pop:    1.04,
  },
}

export const springs = {
  snappy:  { type: "spring", stiffness: 300, damping: 30 },
  gentle:  { type: "spring", stiffness: 120, damping: 14 },
  bouncy:  { type: "spring", stiffness: 400, damping: 10 },
  instant: { type: "spring", stiffness: 600, damping: 35 },
  release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
}

Runtime flags

// lib/motion-config.ts
export const motionConfig = {
  isLowEnd() {
    return (
      typeof navigator !== "undefined" &&
      navigator.hardwareConcurrency <= 4
    )
  },

  prefersReduced() {
    return (
      typeof window !== "undefined" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    )
  },

  shouldAnimate({ essential = false } = {}) {
    if (this.prefersReduced()) return false
    if (!essential && this.isLowEnd()) return false
    return true
  },

  duration() {
    return this.isLowEnd() || this.prefersReduced()
      ? motionTokens.duration.instant
      : motionTokens.duration.normal
  },
}

Accessibility

Priority order (highest to lowest):

  1. prefers-reduced-motion: reduce — disables all transforms, limits opacity transitions to ≤ 0.2s
  2. Low-end device detection — reduces duration, removes non-essential animations
  3. Design preference — everything else

Motion must degrade gracefully. It must never disappear abruptly in a way that causes layout shift or confuses orientation.

// hooks/use-reduced-motion.tsx
"use client"
import { useReducedMotion } from "motion/react"

export function useSafeMotion(fullY: number = 16) {
  const reduce = useReducedMotion()
  return {
    initial: { opacity: 0, y: reduce ? 0 : fullY },
    animate: { opacity: 1, y: 0 },
    exit:    { opacity: 0, y: reduce ? 0 : -fullY },
  }
}
/* globals.css */
@media (prefers-reduced-motion: reduce) {
  .motion-safe-transition  { transition: opacity 0.15s; }
  .motion-reduce-transform { transform: none !important; }
}
<!-- Tailwind -->
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>

SSR / hydration safety

Rule: initial must always match what the server renders.

// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />

// CORRECT — use AnimatePresence or defer to client mount
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])

<motion.div
  initial={{ opacity: mounted ? 0 : 1 }}
  animate={{ opacity: 1 }}
/>

Code Examples

End-to-end: tokens + springs + accessibility + SSR guard

// components/fade-in-card.tsx
"use client"

import { useState, useEffect } from "react"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"
import { motionConfig } from "@/lib/motion-config"

interface FadeInCardProps {
  children: React.ReactNode
  delay?: number
}

export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
  // SSR guard — initial must match server output (opacity: 1)
  const [mounted, setMounted] = useState(false)
  useEffect(() => setMounted(true), [])

  // Accessibility — disables transform when reduced motion is preferred
  const safeMotion = useSafeMotion(motionTokens.distance.md)

  // Device gate — skip animation on low-end hardware
  if (!motionConfig.shouldAnimate() || !mounted) {
    return <div>{children}</div>
  }

  return (
    <motion.div
      initial={safeMotion.initial}
      animate={safeMotion.animate}
      exit={safeMotion.exit}
      transition={{
        ...springs.gentle,
        delay,
      }}
      whileHover={{ scale: motionTokens.scale.pop }}
      whileTap={{ scale: motionTokens.scale.press }}
    >
      {children}
    </motion.div>
  )
}

Constraints / Non-Goals

This skill does not cover:

  • UI component patterns (button, modal, stagger) → see motion-patterns
  • Drag, gestures, SVG, text animations, custom hooks → see motion-advanced
  • CSS-only animations or Tailwind animate-* classes without motion/react
  • Third-party animation libraries (GSAP, anime.js, etc.)
  • Motion design decisions (when to animate, what to emphasize) — that is a design concern, not a code constraint

Anti-Patterns

Related Skills

  • motion-patterns — consumes tokens and springs defined here to build button, modal, stagger, page transition, and scroll patterns. Does not redefine any values.
  • motion-advanced — consumes tokens and springs defined here for drag, SVG, text, and gesture patterns. Adds useAnimate sequences and custom hooks on top of this foundation.

すべてのファイル

1件のファイル

motion-foundationsをインストール

スキルファイルをダウンロードし、.claude/skills/ ディレクトリに解凍してください。

ZIPをダウンロード

リポジトリをクローンし、スキルファイルをプロジェクトにコピーしてください。

git clone https://github.com/affaan-m/ECC/tree/main/skills/motion-foundations # Copy the skill folder to .claude/skills/ or .codex/skills/

コピー コピー
クイックセットアップ: skill フォルダを .claude/skills/ にコピーしてください。Claude が自動的にそのスキルを検出して使用します。
リポジトリ affaan-m/ECC

関連スキル

web-search
更新された時間 2026年6月29日
computer-use
更新された時間 2026年7月29日
webapp-testing
更新された時間 2026年6月29日
lark-base
更新された時間 2026年7月5日
OR