motion-design-patterns について
React プロジェクト向けに Motion のアニメーションパターンを示すスキルです。画面を構築・改善する開発者向けに、意図の明確さ、性能、タイミングの一貫性を重視した実装例を提供します。
主な機能
- 01スプリング設定の参考
- 02フェード・上昇と順次表示
- 03レイアウトと共有レイアウトのアニメーション
- 04ホバーと押下時のインタラクション
- 05スクロール連動とページ遷移
活用シーン
- 01カードグリッドに表示・ホバー効果を追加
- 02タブ切替やアコーディオン展開をアニメーション化
- 03ランディングページにスクロール連動効果を追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dylanfeltus/ skills --skill motion-design-patternsインストール後、Claude Code で/motion-design-patternsを入力して読み込まれたか確認します。
同梱ファイルREADME.md
詳細
モーションデザインパターン
React 向けの Framer Motion(Motion)パターンです。スプリング、スタガー、レイアウトアニメーション、マイクロインタラクション、スクロール連動エフェクト、終了アニメーションを扱います。ありきたりな UI と洗練された UI を分ける最大の要素です。
使用する場面
- React プロジェクトの UI アニメーションを作成または改善する場合
- ユーザーが「洗練させる」「使っていて楽しくする」「マイクロインタラクション」「使い心地をよくする」と依頼した場合
- 表示時/終了時のアニメーション、ホバーエフェクト、ページ遷移を追加する場合
- リスト、カード、モーダル、ナビゲーションを上質な印象にしたい場合
- ユーザーが Magic UI、Motion Primitives、Framer Motion に言及した場合
基本理念
- モーションには目的を持たせます。 すべてのアニメーションは、状態の変化、階層、空間的な関係、フィードバックなどを伝える必要があります。
- 控えめにするほど効果的です。 適切に調整したスプリングを 1 つ使うほうが、互いに競合するアニメーションを 5 つ使うより優れています。
- パフォーマンスを最優先します。 アニメーションは
transformとopacityのみにします。width、height、top、left、marginはアニメーションさせないでください。 - 一貫性が重要です。 プロジェクト全体で同じスプリング設定を使います。
依存関係
npm install motion
インポート:import { motion, AnimatePresence, stagger, useScroll, useTransform } from "motion/react"
注:パッケージ名は 2024 年後半に framer-motion から motion に変更されました。どちらも動作しますが、現在のパッケージは motion です。
スプリング設定
スプリングはイージング曲線よりも自然に感じられます。デフォルトとして以下を使用してください。
推奨デフォルト
// Snappy — buttons, toggles, small elements
const snappy = { type: "spring", stiffness: 500, damping: 30 }
// Smooth — cards, panels, modals
const smooth = { type: "spring", stiffness: 300, damping: 25 }
// Gentle — page transitions, large elements
const gentle = { type: "spring", stiffness: 200, damping: 20 }
// Bouncy — playful UI, notifications, badges
const bouncy = { type: "spring", stiffness: 400, damping: 15 }
クイックリファレンス
| 印象 | stiffness | damping | 用途 |
|---|---|---|---|
| キビキビ | 500 | 30 | ボタン、トグル、チップ |
| 滑らか | 300 | 25 | カード、パネル、モーダル |
| 穏やか | 200 | 20 | ページ遷移、ヒーロー |
| 弾む | 400 | 15 | 通知、バッジ、楽しい UI |
目安
- stiffness を高くする = アニメーションが速くなります
- damping を低くする = より弾みます
- damping ratio < 1 = オーバーシュート(バウンド)します
- バウンドさせない場合は、damping を 2 × √stiffness 以上に設定します
パターン 1:フェード+上昇表示
定番の表示アニメーションです。要素が少し上にスライドしながらフェードインします。
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
Content here
</motion.div>
使用する場面: カード、セクション、ページに表示されるあらゆるコンテンツ。
アンチパターン: y: 100 など大きな値は使わないでください。控えめな値(12~24px)は上質に感じられますが、大きな値はぎこちなく見えます。
パターン 2:スタガー付きリスト
子要素を 1 つずつアニメーションさせます。リストに生き生きとした印象を与える連鎖効果です。
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.08,
delayChildren: 0.1,
},
},
}
const item = {
hidden: { opacity: 0, y: 16 },
visible: {
opacity: 1,
y: 0,
transition: { type: "spring", stiffness: 300, damping: 25 },
},
}
<motion.ul variants={container} initial="hidden" animate="visible">
{items.map((i) => (
<motion.li key={i.id} variants={item}>
{i.content}
</motion.li>
))}
</motion.ul>
タイミングの目安:
- 3~5 個の項目:
staggerChildren: 0.1 - 6~12 個の項目:
staggerChildren: 0.06 - 12 個以上:
staggerChildren: 0.03(またはグループとしてアニメーション)
アンチパターン: 個別にスタガーさせる項目を約 15 個以上にしないでください。遅く感じられます。グループ化するか、波状のエフェクトを使います。
パターン 3:レイアウトアニメーション
レイアウト状態間を自動的にアニメーションさせます。Motion の最大の特長です。
<motion.div layout transition={{ type: "spring", stiffness: 300, damping: 25 }}>
{isExpanded ? <ExpandedContent /> : <CollapsedContent />}
</motion.div>
共有レイアウト(タブ、アクティブインジケーター)
{tabs.map((tab) => (
<button key={tab.id} onClick={() => setActive(tab.id)}>
{tab.label}
{active === tab.id && (
<motion.div
layoutId="activeTab"
className="absolute inset-0 bg-primary rounded-md"
transition={{ type: "spring", stiffness: 400, damping: 30 }}
/>
)}
</button>
))}
使用する場面: タブインジケーター、展開するカード、並べ替え可能なリスト、フィルタリングするグリッド。
パフォーマンスのヒント: 位置のみ(サイズは対象外)をアニメーションさせる場合は、layout="position" を追加します。こちらのほうが処理負荷を抑えられます。
パターン 4:終了アニメーション(AnimatePresence)
DOM から削除される前に、要素をアニメーションで退出させます。
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
Modal content
</motion.div>
)}
</AnimatePresence>
AnimatePresence のモード
| モード | 動作 | 用途 |
|---|---|---|
"sync"(デフォルト) | 表示と終了を同時に行います | クロスフェードエフェクト |
"wait" | 表示を開始する前に終了の完了を待ちます | ページ遷移、モーダル |
"popLayout" | 終了する要素をレイアウトフローから取り除きます | 項目を削除するリスト |
パターン 5:ホバーとタップのマイクロインタラクション
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
>
Click me
</motion.button>
要素タイプ別のホバーパターン
| 要素 | whileHover | whileTap |
|---|---|---|
| ボタン(プライマリ) | { scale: 1.02 } | { scale: 0.98 } |
| カード | { y: -4, boxShadow: "0 8px 30px rgba(0,0,0,0.12)" } | — |
| アイコンボタン | { scale: 1.1 } | { scale: 0.9 } |
| リンク | { x: 2 } | — |
| アバター | { scale: 1.05 } | — |
アンチパターン: ボタンのスケールを 1.05 より大きくしないでください。漫画のような印象になります。控えめな値(1.01~1.03)のほうが上質に感じられます。
パターン 6:スクロールでトリガーされるアニメーション
ビューに入るときのアニメーション
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ type: "spring", stiffness: 200, damping: 20 }}
>
Appears when scrolled into view
</motion.div>
viewport.once: true — 最初の一度だけアニメーションします(ランディングページで最も一般的です)。
viewport.margin — 負のマージンを指定すると、要素が完全に表示される前にトリガーされます。
スクロールに連動する進行状況
const { scrollYProgress } = useScroll()
const opacity = useTransform(scrollYProgress, [0, 0.3], [1, 0])
const y = useTransform(scrollYProgress, [0, 0.3], [0, -50])
<motion.div style={{ opacity, y }}>
Parallax hero content
</motion.div>
パターン 7:ページ遷移
// In your layout or page wrapper
<AnimatePresence mode="wait">
<motion.main
key={pathname}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
{children}
</motion.main>
</AnimatePresence>
控えめにしましょう。 ページ遷移は短く(体感で 200~300ms)、動きも小さく(8~12px)します。派手なページ遷移は、2015 年のような印象を与えます。
パターン 8:数値/テキストの遷移
// Animate a counter
<motion.span
key={count}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
>
{count}
</motion.span>
退出アニメーションのために、AnimatePresence で囲みます。ダッシュボード、料金表示、ライブデータに最適です。
避けるべきアンチパターン
| ❌ してはいけないこと | ✅ 代わりにすること |
|---|---|
width/height を直接アニメーションさせる | scale または layout アニメーションを使う |
大きな移動量(y: 200) | 控えめな値(y: 16–24) |
| 何にでもバウンスを使う | 遊び心や祝福を表現する場面に限ってバウンスを使う |
| スクロールイベントのたびにアニメーションさせる | once: true を指定して whileInView を使う |
| 要素ごとにタイミングを変える | プロジェクト全体で一貫したスプリング設定を使う |
| ページ読み込み時にすべてをアニメーションさせる | ファーストビューを優先し、残りは順番に表示する |
| カスタムのイージングカーブ | スプリングを使う — 割り込みにうまく応答します |
おすすめのコンポーネントライブラリの参考情報
アニメーション付きコンポーネントを作成する際は、パターンやアイデアの参考として以下を確認してください。
- Magic UI — アニメーション付き React コンポーネントを 150 種類以上収録。shadcn/ui に対応
- Motion Primitives — React 用のモーションコンポーネントをコピーして貼り付けて使えます
- Aceternity UI — トレンド感のあるアニメーション付きコンポーネント(より重量感があり、演出も強め)
ユーザーが特定のアニメーション付きコンポーネント(テキストの表示、アニメーション付きボーダー、グラデーションアニメーションなど)を求めている場合は、まずこれらのライブラリを確認してください。実績のある実装が見つかる可能性があります。
選択のクイックガイド
| シナリオ | パターン | スプリング設定 |
|---|---|---|
| カードの表示 | フェード+上昇 | smooth |
| リストの読み込み | 順次表示するリスト | smooth、0.08s の間隔 |
| タブの切り替え | 共有レイアウト(layoutId) | snappy |
| モーダルの開閉 | AnimatePresence+スケール | smooth |
| ボタンの押下 | whileHover+whileTap | snappy |
| ランディングページのセクション | スクロールでトリガー | gentle |
| ページ移動 | ページ遷移 | smooth |
| ダッシュボードのカウンター | 数値の遷移 | snappy |
| 通知ポップアップ | フェード+上昇+バウンス | bouncy |
| アコーディオンの展開 | レイアウトアニメーション | smooth |
例
例 1:「このカードグリッドにアニメーションを追加する」
グリッドにフェード+上昇の順次表示を適用し、各カードにホバー時の浮き上がり効果を追加します。
<motion.div
variants={container}
initial="hidden"
animate="visible"
className="grid grid-cols-3 gap-4"
>
{cards.map((card) => (
<motion.div
key={card.id}
variants={item}
whileHover={{ y: -4, boxShadow: "0 8px 30px rgba(0,0,0,0.12)" }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
<Card {...card} />
</motion.div>
))}
</motion.div>
例 2:「このモーダルの見栄えをよくする」
AnimatePresence で囲み、表示/退出時にスケール+不透明度のアニメーションを追加し、オーバーレイをフェードさせます。
<AnimatePresence>
{isOpen && (
<>
<motion.div
className="fixed inset-0 bg-black/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
<motion.div
className="fixed inset-0 flex items-center justify-center"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
<ModalContent onClose={onClose} />
</motion.div>
</>
)}
</AnimatePresence>
例 3:「このランディングページにスクロールアニメーションを追加する」
各セクションに、子要素を順次表示する whileInView を適用します。
<motion.section
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ type: "spring", stiffness: 200, damping: 20 }}
>
<h2>Feature Section</h2>
<motion.div
variants={container}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{features.map((f) => (
<motion.div key={f.id} variants={item}>
<FeatureCard {...f} />
</motion.div>
))}
</motion.div>
</motion.section>