オプション

motion-ui

affaan-m/ECC affaan-m/ECC

React/Next.js 向けに、パフォーマンス、アクセシビリティ、およびユーザビリティに関するガイドラインを備えた、本番環境対応の UI モーションシステムを提供します。

...すべて拡張します
0
更新された時間 2026年10月1日

Motion System v4.2

React / Next.js 向けの、本番環境対応の UI モーションシステム。

装飾ではなく、パフォーマンス、アクセシビリティ、ユーザビリティに重点を置いています。

使用すべき場面

以下の場合に、このモーションシステムをご利用ください:

  • ユーザーの注意を誘導する場合(例:オンボーディング、主要なアクション)
  • 状態を伝える場合(読み込み中、成功、エラー、遷移)
  • 空間的な連続性を維持する場合(レイアウトの変更、ナビゲーションなど)

適切な使用場面

  • インタラクティブなコンポーネント(ボタン、モーダル、メニュー)
  • 状態遷移(読み込み中 → 読み込み完了、開いている → 閉じている)
  • ナビゲーションとレイアウトの連続性(共通要素、クロスフェード)

考慮事項

  • アクセシビリティ:常に動きの抑制に対応する
  • デバイスへの適応:低スペック端末向けに調整する
  • パフォーマンスのトレードオフ:視覚的な滑らかさよりも応答性を優先する

以下の場合はモーションの使用を避ける

  • 純粋に装飾的な場合
  • ユーザビリティや明瞭性を損なう場合
  • パフォーマンスに悪影響を及ぼす場合

仕組み

基本原則

モーションは以下の条件を満たさなければならない:

  • 注意を誘導すること
  • 状態を伝える
  • 空間的な連続性を保つ

これらをすべて満たさない場合は、それを削除する。

設置

npm install motion

バージョン

  • motion/react - 現在の Motion for React プロジェクトのデフォルト(パッケージ: motion)
  • framer-motion - 依然としてFramer Motionに依存しているプロジェクト向けのレガシーインポートパス

混在させないでください。混在させると、内部スケジューラ間で競合が発生し、 AnimatePresence コンテキストの破損を引き起こします。つまり、一方のパッケージのコンポーネントは、もう一方のパッケージのコンポーネントと終了アニメーションを連携させることができなくなります。

プロジェクトで使用されているバージョンを確認するには:

cat package.json | grep -E '"motion"|"framer-motion"'

常に一貫して1つのソースからインポートしてください:

// Correct (modern)
import { motion, AnimatePresence } from "motion/react"

// Correct (legacy)
import { motion, AnimatePresence } from "framer-motion"

// Never mix both in the same project

Motion Tokens

// motionTokens.ts
export const motionTokens = {
  duration: {
    fast: 0.18,
    normal: 0.35,
    slow: 0.6
  },
  // Use these as the `ease` value inside a `transition` object:
  // transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
  easing: {
    smooth: [0.22, 1, 0.36, 1] as [number, number, number, number],
    sharp:  [0.4,  0, 0.2, 1] as [number, number, number, number]
  },
  distance: {
    sm: 8,
    md: 16,
    lg: 24
  }
}

使用例:

import { motionTokens } from "@/lib/motionTokens"


パフォーマンスに関するルール

安全

  • transform
  • 不透明度

避ける

  • 幅 / 高さ
  • top / left

ルール:レスポンシブ性 > 滑らかさ

デバイスへの適応

このヒューリスティックは、CPUコア数と利用可能なメモリを組み合わせて、より信頼性の高いシグナルを生成します。 deviceMemory Chrome/Android で利用可能。フォールバックは Safari および Firefox をカバーします。

const isLowEnd =
  typeof navigator !== "undefined" && (
    // Low memory (Chrome/Android only; undefined elsewhere → treat as capable)
    (navigator.deviceMemory !== undefined && navigator.deviceMemory <= 2) ||
    // Few cores AND no memory API (covers Safari/Firefox on weak hardware)
    (navigator.deviceMemory === undefined && navigator.hardwareConcurrency <= 4)
  )

const duration = isLowEnd ? 0.2 : 0.4

アクセシビリティ

JS (useReducedMotion)

import { motion, useReducedMotion } from "motion/react"

export function FadeIn() {
  const reduce = useReducedMotion()

  return (
    
  )
}

CSS

@media (prefers-reduced-motion: reduce) {
  .motion-safe-transition {
    transition: opacity 0.2s;
  }

  .motion-reduce-transform {
    transform: none !important;
  }
}

Tailwind

アーキテクチャとパターン

コアパターン

シナリオ パターン
ホバー時のフィードバック whileHover
タップ/押下時のフィードバック whileTap
スクロール時の表示 whileInView
スクロール連動値 useScroll + useTransform
条件付きマウント/アンマウント AnimatePresence
わずかなレイアウトのずれ(単一要素、変更幅 < ~300px) layout プロパティ
大幅なレイアウトのずれ、またはページ全体のリフロー 避ける layout; 代わりにCSSトランジションまたはページ単位のルーティングを使用する
複雑で命令型のシーケンス useAnimate

大きなコンテナでlayoutを避けるべき理由は? Framerのレイアウトアニメーションは transform を使用して位置を調整しますが、ビューポート全体にまたがる要素や大規模な再配置を引き起こす要素では、測定コストにより目に見えるカクつきやCLSが発生します。代わりにCSS Grid/Flexboxのトランジションを使用するか、 layoutId を特定の子要素に限定して使用してください。

レイアウトとトランジション

  • 共有要素のトランジション → layoutId (マウントされたインスタンスごとに一意である必要がある)
  • エントリー/エグジット遷移 → AnimatePresence ( mode 以下のガイダンスを参照)

AnimatePresence mode

常に mode 明示的に指定してください — デフォルト設定("sync")では、進入と退出が同時に実行されるため、ほとんどのUIパターンで視覚的な重複が生じます。

mode 使用すべき場面
"wait" 「exit」が完了してから「enter」が開始されます。モーダル、トースト、ページ遷移に使用します。
"sync" (デフォルト) 「enter」と「exit」が重なります。重なりが意図されている場合(例:クロスフェードするカルーセル)にのみ使用してください。
"popLayout" 終了する要素はフローから即座にポップアウトされ、残りのアイテムがアニメーションでそのスペースを埋めます。リスト、タブ、閉じられるカードなどに使用します。
// Modal — always use "wait"

  {open && }


// Dismissible list item — use "popLayout"

  {items.map(item => )}

高度なパターン(概念)

  • パララックス(スクロール連動の変形)
  • スクロールストーリーテリング(スティッキーセクション)
  • 3Dチルト(ポインタに基づく変形)
  • クロスフェード(共有 layoutId)
  • プログレッシブ・リビール(クリップパス)
  • スケルトンローディング(ループする不透明度)
  • マイクロインタラクション(ホバー/タップ時のフィードバック)
  • スプリングシステム(物理演算に基づく動き)

モーダルの基本

  • フォーカストラップ
  • Escで閉じる
  • スクロールロック
  • ARIAロール
  • 使用 AnimatePresence mode="wait" これにより、次のモーダルが表示される前に終了アニメーションが完了する

完全な例

import React, { useEffect, useRef, useState } from "react"
import { motion, AnimatePresence } from "motion/react"

function useFocusTrap(ref: React.RefObject, active: boolean) {
  useEffect(() => {
    if (!active || !ref.current) return
    const el = ref.current
    const focusable = el.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
    const first = focusable[0]
    const last  = focusable[focusable.length - 1]

    function handleKey(e: KeyboardEvent) {
      if (e.key !== "Tab") return
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault()
        last?.focus()
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault()
        first?.focus()
      }
    }

    el.addEventListener("keydown", handleKey)
    first?.focus()
    return () => el.removeEventListener("keydown", handleKey)
  }, [active, ref])
}

function useScrollLock(active: boolean) {
  useEffect(() => {
    if (!active) return
    const prev = document.body.style.overflow
    document.body.style.overflow = "hidden"
    return () => { document.body.style.overflow = prev }
  }, [active])
}

function Modal({ open, closeModal }: { open: boolean; closeModal: () => void }) {
  const ref = useRef(null)

  useFocusTrap(ref, open)
  useScrollLock(open)

  useEffect(() => {
    function onKey(e: KeyboardEvent) {
      if (e.key === "Escape") closeModal()
    }
    if (open) window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  }, [open, closeModal])

  return (
    // mode="wait" ensures exit animation finishes before any new modal enters
    
      {open && (
        
          
            
            
      )}
    
  )
}

export function Example() {
  const [open, setOpen] = useState(false)

  return (
    <>
      

SSRの安全性

  • サーバー側とクライアント側のレンダリングにおける初期状態を一致させる
  • 暗黙的なアニメーションの起点の指定を避ける(常に initial 明示的に設定する)
  • モーションコンポーネントを "use client" Next.js App Router

デバッグ

確認事項:

  • インポートの誤り( motion/react および framer-motion)
  • 欠落している "use client" ディレクティブの欠落
  • Next.js アプリルーターで key プロパティが欠落している AnimatePresence 子要素の
  • ハイドレーションの不一致(SSRとクライアント側の初期状態が異なる)
  • layout 大規模なコンテナでのプロパティの誤使用により、リフロー時のカクつきが発生
  • 状態駆動型アニメーションがトリガーされない(依存関係配列を確認)

QA

  • CLSなし
  • キーボードが動作する
  • モーダルウィンドウ内でフォーカスが閉じ込められる
  • ARIAロールが正しい(role="dialog", aria-modal="true")
  • モーションの抑制が反映されている(useReducedMotion + CSSメディアクエリ)
  • Next.jsでハイドレーションに関する警告が発生しない
  • アンマウント時にアニメーションがきれいに停止(メモリリークなし)
  • AnimatePresence mode すべての使用箇所で明示的に設定

アンチパターン

  • レイアウトプロパティのアニメーション化(width, height, top, left)
  • 目的のない無限のアニメーション(常に自問する:「これはどのような状態を伝えているのか?」)
  • リストの表示タイミングを過度にずらす( staggerChildren 0.1秒以下に抑える。それ以上だと遅く感じられる)
  • モーション抑制の設定を無視する
  • layout 大きなコンテナやビューポート全体を覆うコンテナでの使用
  • ` mode を AnimatePresence (デフォルト "sync" を省略すると、視覚的な重なりが生じます)
  • 装飾目的のみでモーションを使用すること

理念

モーションとはインタラクションデザインである。

最終ルール

モーションがUXを向上させない場合 → 削除する。

例

ボタンのインタラクション

import { motion } from "motion/react"

export function Button() {
  return (
    
      Click me
    
  )
}

モーションを削減した例

import { motion, useReducedMotion } from "motion/react"

export function FadeIn() {
  const reduce = useReducedMotion()

  return (
    
  )
}

リストの段差配置

import { motion } from "motion/react"

const container = {
  hidden: {},
  visible: {
    transition: { staggerChildren: 0.08 } // keep ≤ 0.1s to avoid sluggishness
  }
}

const item = {
  hidden:  { opacity: 0, y: 10 },
  visible: { opacity: 1, y: 0,  transition: { duration: 0.3, ease: [0.22, 1, 0.36, 1] } }
}

export function List() {
  return (
    
      {[1, 2, 3].map(i => (
        Item {i}
      ))}
    
  )
}

AnimatePresence を使用したモーダル

import { motion, AnimatePresence } from "motion/react"

export function Modal({ open }: { open: boolean }) {
  return (
    
      {open && (
        
      )}
    
  )
}

スクロールパララックス

import { useScroll, useTransform, motion } from "motion/react"

export function Parallax() {
  const { scrollYProgress } = useScroll()
  const y = useTransform(scrollYProgress, [0, 1], [0, -80])

  return 
}

スケルトンローディング

import { motion } from "motion/react"

export function Skeleton() {
  return (
    
  )
}

共有レイアウト(クロスフェード)

import { motion } from "motion/react"

// layoutId must be unique per mounted instance.
// If multiple instances can exist simultaneously, append a unique id:
// layoutId={`shared-${item.id}`}
export function Shared() {
  return 
}
GitHubで見る
---
name: motion-ui
description: Provides a production-ready UI motion system for React/Next.js with performance, accessibility, and usability guidelines.
---

# Motion System v4.2

Production-ready UI motion system for React / Next.js.

Focused on **performance, accessibility, and usability** — not decoration.

## When to Use

Use this motion system when motion:

* Guides attention (e.g., onboarding, key actions)
* Communicates state (loading, success, error, transitions)
* Preserves spatial continuity (layout changes, navigation)

### Appropriate Scenarios

* Interactive components (buttons, modals, menus)
* State transitions (loading → loaded, open → closed)
* Navigation and layout continuity (shared elements, crossfade)

### Considerations

* **Accessibility**: Always support reduced motion
* **Device adaptation**: Adjust for low-end devices
* **Performance trade-offs**: Prefer responsiveness over visual smoothness

### Avoid Using Motion When

* It is purely decorative
* It reduces usability or clarity
* It impacts performance negatively

---

## How It Works

### Core Principle

Motion must:

* Guide attention
* Communicate state
* Preserve spatial continuity

If it does none → remove it.

---

### Installation

```bash
npm install motion
```

---

### Version

* `motion/react` - default for current Motion for React projects (package: `motion`)
* `framer-motion` - legacy import path for projects that still depend on Framer Motion

**Do not mix.** Mixing causes conflicting internal schedulers and broken `AnimatePresence` contexts — components from one package will not coordinate exit animations with components from the other.

To check which version your project uses:

```bash
cat package.json | grep -E '"motion"|"framer-motion"'
```

Always import from one source consistently:

```ts
// Correct (modern)
import { motion, AnimatePresence } from "motion/react"

// Correct (legacy)
import { motion, AnimatePresence } from "framer-motion"

// Never mix both in the same project
```

---

### Motion Tokens

```ts
// motionTokens.ts
export const motionTokens = {
  duration: {
    fast: 0.18,
    normal: 0.35,
    slow: 0.6
  },
  // Use these as the `ease` value inside a `transition` object:
  // transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
  easing: {
    smooth: [0.22, 1, 0.36, 1] as [number, number, number, number],
    sharp:  [0.4,  0, 0.2, 1] as [number, number, number, number]
  },
  distance: {
    sm: 8,
    md: 16,
    lg: 24
  }
}
```

Usage example:

```tsx
import { motionTokens } from "@/lib/motionTokens"

<motion.div
  initial={{ opacity: 0, y: motionTokens.distance.md }}
  animate={{ opacity: 1, y: 0 }}
  transition={{
    duration: motionTokens.duration.normal,
    ease: motionTokens.easing.smooth
  }}
/>
```

---

### Performance Rules

**Safe**

* transform
* opacity

**Avoid**

* width / height
* top / left

Rule: responsiveness > smoothness

---

### Device Adaptation

The heuristic combines CPU core count **and** available memory for a more reliable signal. `deviceMemory` is available on Chrome/Android; the fallback covers Safari and Firefox.

```ts
const isLowEnd =
  typeof navigator !== "undefined" && (
    // Low memory (Chrome/Android only; undefined elsewhere → treat as capable)
    (navigator.deviceMemory !== undefined && navigator.deviceMemory <= 2) ||
    // Few cores AND no memory API (covers Safari/Firefox on weak hardware)
    (navigator.deviceMemory === undefined && navigator.hardwareConcurrency <= 4)
  )

const duration = isLowEnd ? 0.2 : 0.4
```

---

### Accessibility

#### JS (useReducedMotion)

```tsx
import { motion, useReducedMotion } from "motion/react"

export function FadeIn() {
  const reduce = useReducedMotion()

  return (
    <motion.div
      initial={{ opacity: 0, y: reduce ? 0 : 24 }}
      animate={{ opacity: 1, y: 0 }}
    />
  )
}
```

#### CSS

```css
@media (prefers-reduced-motion: reduce) {
  .motion-safe-transition {
    transition: opacity 0.2s;
  }

  .motion-reduce-transform {
    transform: none !important;
  }
}
```

#### Tailwind

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

---

### Architecture & Patterns

#### Core Patterns

| Scenario | Pattern |
|---|---|
| Hover feedback | `whileHover` |
| Tap / press feedback | `whileTap` |
| Reveal on scroll | `whileInView` |
| Scroll-linked value | `useScroll` + `useTransform` |
| Conditional mount/unmount | `AnimatePresence` |
| Small layout shifts (single element, < ~300px change) | `layout` prop |
| Large layout shifts or full-page reflows | Avoid `layout`; use CSS transitions or page-level routing instead |
| Complex, imperative sequences | `useAnimate` |

> **Why avoid `layout` on large containers?** Framer's layout animation uses `transform` to reconcile positions, but on elements that span the full viewport or trigger deep reflow, the measurement cost causes visible jank and CLS. Prefer CSS Grid/Flexbox transitions or coordinate with `layoutId` on specific child elements only.

#### Layout & Transitions

* Shared element transitions → `layoutId` (must be unique per mounted instance)
* Enter / exit transitions → `AnimatePresence` (see `mode` guidance below)

#### AnimatePresence `mode`

Always specify `mode` explicitly — the default (`"sync"`) runs enter and exit simultaneously, which causes visual overlap in most UI patterns.

| `mode` | When to use |
|---|---|
| `"wait"` | Exit completes before enter starts. Use for **modals, toasts, page transitions**. |
| `"sync"` (default) | Enter and exit overlap. Use only when overlap is intentional (e.g., crossfade carousels). |
| `"popLayout"` | Exiting element is popped out of flow immediately; remaining items animate to fill. Use for **lists, tabs, dismissible cards**. |

```tsx
// Modal — always use "wait"
<AnimatePresence mode="wait">
  {open && <Modal key="modal" />}
</AnimatePresence>

// Dismissible list item — use "popLayout"
<AnimatePresence mode="popLayout">
  {items.map(item => <Card key={item.id} />)}
</AnimatePresence>
```

---

### Advanced Patterns (Concepts)

* Parallax (scroll-linked transforms)
* Scroll storytelling (sticky sections)
* 3D tilt (pointer-based transforms)
* Crossfade (shared `layoutId`)
* Progressive reveal (clip-path)
* Skeleton loading (looped opacity)
* Micro-interactions (hover/tap feedback)
* Spring system (physics-based motion)

---

### Modal Essentials

* Focus trap
* Escape close
* Scroll lock
* ARIA roles
* Use `AnimatePresence mode="wait"` so exit animation completes before the next modal enters

#### Full Example

```tsx
import React, { useEffect, useRef, useState } from "react"
import { motion, AnimatePresence } from "motion/react"

function useFocusTrap(ref: React.RefObject<HTMLDivElement | null>, active: boolean) {
  useEffect(() => {
    if (!active || !ref.current) return
    const el = ref.current
    const focusable = el.querySelectorAll<HTMLElement>(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
    const first = focusable[0]
    const last  = focusable[focusable.length - 1]

    function handleKey(e: KeyboardEvent) {
      if (e.key !== "Tab") return
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault()
        last?.focus()
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault()
        first?.focus()
      }
    }

    el.addEventListener("keydown", handleKey)
    first?.focus()
    return () => el.removeEventListener("keydown", handleKey)
  }, [active, ref])
}

function useScrollLock(active: boolean) {
  useEffect(() => {
    if (!active) return
    const prev = document.body.style.overflow
    document.body.style.overflow = "hidden"
    return () => { document.body.style.overflow = prev }
  }, [active])
}

function Modal({ open, closeModal }: { open: boolean; closeModal: () => void }) {
  const ref = useRef<HTMLDivElement>(null)

  useFocusTrap(ref, open)
  useScrollLock(open)

  useEffect(() => {
    function onKey(e: KeyboardEvent) {
      if (e.key === "Escape") closeModal()
    }
    if (open) window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  }, [open, closeModal])

  return (
    // mode="wait" ensures exit animation finishes before any new modal enters
    <AnimatePresence mode="wait">
      {open && (
        <motion.div
          role="dialog"
          aria-modal="true"
          aria-labelledby="modal-title"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{ duration: 0.2 }}
          className="fixed inset-0 flex items-center justify-center bg-black/40"
        >
          <motion.div
            ref={ref}
            initial={{ scale: 0.95, opacity: 0 }}
            animate={{ scale: 1,    opacity: 1 }}
            exit={{    scale: 0.95, opacity: 0 }}
            transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
            className="bg-white p-6 rounded"
          >
            <h2 id="modal-title">Dialog Title</h2>
            <button onClick={closeModal}>Close</button>
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  )
}

export function Example() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <button onClick={() => setOpen(true)}>Open</button>
      <Modal open={open} closeModal={() => setOpen(false)} />
    </>
  )
}
```

---

### SSR Safety

* Match initial states between server and client renders
* Avoid implicit animation origins (always set `initial` explicitly)
* Wrap motion components in `"use client"` in Next.js App Router

---

### Debugging

Check:

* Wrong import (mixing `motion/react` and `framer-motion`)
* Missing `"use client"` directive in Next.js App Router
* Missing `key` prop on `AnimatePresence` children
* Hydration mismatch (initial state differs between SSR and client)
* `layout` prop misuse on large containers causing reflow jank
* State-driven animation not triggering (check dependency arrays)

---

### QA

* No CLS
* Keyboard works
* Focus trapped in modals
* ARIA roles correct (`role="dialog"`, `aria-modal="true"`)
* Reduced motion respected (`useReducedMotion` + CSS media query)
* No hydration warnings in Next.js
* Animations stop cleanly on unmount (no memory leaks)
* `AnimatePresence mode` set explicitly on all usage sites

---

### Anti-Patterns

* Animating layout properties (`width`, `height`, `top`, `left`)
* Infinite animations without purpose (always ask: what state does this communicate?)
* Over-staggering lists (keep `staggerChildren` ≤ 0.1s; beyond that it feels slow)
* Ignoring reduced motion preferences
* Using `layout` on large or full-viewport containers
* Omitting `mode` on `AnimatePresence` (default `"sync"` causes visual overlap)
* Using motion purely for decoration

---

### Philosophy

Motion is interaction design.

---

### Final Rule

> If motion does not improve UX → remove it.

---

## Examples

### Button Interaction

```tsx
import { motion } from "motion/react"

export function Button() {
  return (
    <motion.button
      whileHover={{ scale: 1.02 }}
      whileTap={{ scale: 0.97 }}
      transition={{ duration: 0.15, ease: [0.4, 0, 0.2, 1] }}
    >
      Click me
    </motion.button>
  )
}
```

---

### Reduced Motion Example

```tsx
import { motion, useReducedMotion } from "motion/react"

export function FadeIn() {
  const reduce = useReducedMotion()

  return (
    <motion.div
      initial={{ opacity: 0, y: reduce ? 0 : 24 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: reduce ? 0.1 : 0.35, ease: [0.22, 1, 0.36, 1] }}
    />
  )
}
```

---

### Stagger List

```tsx
import { motion } from "motion/react"

const container = {
  hidden: {},
  visible: {
    transition: { staggerChildren: 0.08 } // keep ≤ 0.1s to avoid sluggishness
  }
}

const item = {
  hidden:  { opacity: 0, y: 10 },
  visible: { opacity: 1, y: 0,  transition: { duration: 0.3, ease: [0.22, 1, 0.36, 1] } }
}

export function List() {
  return (
    <motion.ul variants={container} initial="hidden" animate="visible">
      {[1, 2, 3].map(i => (
        <motion.li key={i} variants={item}>Item {i}</motion.li>
      ))}
    </motion.ul>
  )
}
```

---

### Modal with AnimatePresence

```tsx
import { motion, AnimatePresence } from "motion/react"

export function Modal({ open }: { open: boolean }) {
  return (
    <AnimatePresence mode="wait">
      {open && (
        <motion.div
          initial={{ opacity: 0, scale: 0.95 }}
          animate={{ opacity: 1, scale: 1    }}
          exit={{    opacity: 0, scale: 0.95 }}
          transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
        />
      )}
    </AnimatePresence>
  )
}
```

---

### Scroll Parallax

```tsx
import { useScroll, useTransform, motion } from "motion/react"

export function Parallax() {
  const { scrollYProgress } = useScroll()
  const y = useTransform(scrollYProgress, [0, 1], [0, -80])

  return <motion.div style={{ y }} />
}
```

---

### Skeleton Loading

```tsx
import { motion } from "motion/react"

export function Skeleton() {
  return (
    <motion.div
      className="bg-gray-200 h-6 w-full rounded"
      animate={{ opacity: [0.5, 1, 0.5] }}
      transition={{
        duration: 1.5,       // comfortable pulse — was missing, caused fast flash
        repeat: Infinity,
        ease: "easeInOut"
      }}
    />
  )
}
```

---

### Shared Layout (Crossfade)

```tsx
import { motion } from "motion/react"

// layoutId must be unique per mounted instance.
// If multiple instances can exist simultaneously, append a unique id:
// layoutId={`shared-${item.id}`}
export function Shared() {
  return <motion.div layoutId="shared" />
}
```

すべてのファイル

1件のファイル

motion-uiをインストール

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

ZIPをダウンロード

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

git clone https://github.com/affaan-m/ECC/tree/main/skills/motion-ui # Copy SKILL.md to your .claude/skills/ directory

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

関連スキル

github-code-search
更新された時間 2026年6月29日
drizzle-orm
更新された時間 2026年6月29日
prisma-client-api
更新された時間 2026年6月29日
clickhouse-io
更新された時間 2026年6月29日
OR