motion-design について
UIアニメーションやマイクロインタラクションを作るエージェント向けのモーションデザインスキルです。トランジション、スクロール演出、ローディングのパターンに加え、パフォーマンスとモーション軽減への対応を扱います。
主な機能
- 01アニメーション時間とイージングの指針
- 02ボタン、トグル、画面遷移のパターン
- 03スクロール連動とローディングのパターン
- 04モーション軽減への対応方法
- 05アニメーションの性能最適化と検証
活用シーン
- 01ボタンやトグルに操作フィードバックを追加
- 02画面表示やリストの段階的表示を設計
- 03ローディング用プレースホルダーや表示を作成
- 04モーション軽減時の動作と性能を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ arnavpuri/ designskills --skill motion-designインストール後、Claude Code で/motion-designを入力して読み込まれたか確認します。
詳細
モーションデザイン
パフォーマンスを損なうことなく UX を向上させる、意図的なアニメーションとマイクロインタラクションを追加します。
前提条件
.agents/design-context.md(design-contextを参照)を読みます。スタイルのアーキタイプによってモーションの特性が決まります(Minimal/Corporate=短く、オーバーシュートなし、Playful=スプリングを使用可、Luxurious=ゆっくりとした滑らかなイーズアウト)。- デザインシステムがある場合は、そのトランジショントークンを使用します。以下のトークン名(
--duration-*、--ease-*)は、design-system スキルで定義されているものと同じです。 - 常に
prefers-reduced-motionを尊重します。
ステップ 1:UI アニメーションの原則
3 つのルール
- すべてのアニメーションには目的が必要です:注意を誘導する、フィードバックを提供する、関連性を示す、または状態の変化を伝えます。
- 速いほど、ほとんどの場合、優れています:UI アニメーションは 100~500ms にします。
- 変化するものをアニメーションさせます:実際に変化するプロパティだけをアニメーションさせます。
所要時間のガイドライン
| 操作 | 所要時間 |
|---|---|
| ボタンのホバー/押下 | 100-150ms |
| トグル/スイッチ | 200ms |
| ドロップダウン/メニューを開く | 200-250ms |
| モーダル/ダイアログの表示 | 250-300ms |
| ページ遷移 | 300-400ms |
| 展開/折りたたみ | 250-350ms |
| ローディングスピナーの周期 | 700-1200ms |
終了時のアニメーションは、対応する開始時のアニメーションより約 20~30%短くします(例:モーダルの表示は 250ms、非表示は 200ms)。大きな移動や長距離の移動には、小さな移動より時間がかかります。モバイルでの所要時間は通常、デスクトップ/タブレットより短くします。
イージング関数
:root {
--duration-fast: 100ms; --duration-normal: 200ms; --duration-slow: 300ms; --duration-slower: 500ms;
--ease-out: cubic-bezier(0, 0, 0.2, 1); /* Decelerate: ENTER */
--ease-in: cubic-bezier(0.4, 0, 1, 1); /* Accelerate: EXIT */
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); /* Standard: MOVE */
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* Bouncy overshoot */
--ease-bounce: cubic-bezier(0.68, -0.55, 0.27, 1.55); /* Strong bounce */
}
開始時には ease-out、終了時には ease-in、移動には ease-in-out、遊び心のあるフィードバックにはスプリングを使用します。
ステップ 2:マイクロインタラクションのパターン
ボタンの押下
.btn {
transition: transform 0.1s var(--ease-out), background-color 0.15s var(--ease-out), box-shadow 0.15s var(--ease-out);
}
.btn:hover { box-shadow: 0 4px 12px oklch(from var(--interactive-primary) l c h / 0.3); }
.btn:active { transform: scale(0.97); }
トグルスイッチ
.toggle-track { width: 44px; height: 24px; background: var(--gray-300); border-radius: 12px; padding: 2px; cursor: pointer; transition: background-color 0.2s var(--ease-out); }
.toggle-thumb { width: 20px; height: 20px; background: white; border-radius: 50%; transition: transform 0.2s var(--ease-spring); }
.toggle-input:checked + .toggle-track { background: var(--brand-500); }
.toggle-input:checked + .toggle-track .toggle-thumb { transform: translateX(20px); }
.toggle-input:focus-visible + .toggle-track { outline: 2px solid var(--interactive-primary); outline-offset: 2px; }
.toggle-input は <label> 内にある実際の <input type="checkbox" role="switch"> でなければなりません。キーボードとスクリーンリーダーで操作できるよう、視覚的に非表示にします(display: none は使用しません)。
いいね/ハートのアニメーション
.like-btn.is-liked .heart-icon { animation: like-pop 0.4s var(--ease-spring); color: #ef4444; fill: currentColor; }
@keyframes like-pop { 0% { transform: scale(1); } 30% { transform: scale(1.3); } 60% { transform: scale(0.9); } 100% { transform: scale(1); } }
ステップ 3:ページ遷移アニメーション
上方向にスライドしてフェード
.page-enter { animation: slideUpFade 0.4s var(--ease-out) both; }
@keyframes slideUpFade {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
View Transitions API(モダン)
/* Cross-document (MPA, same origin): opt in on BOTH pages */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation: vt-fade-out 0.2s var(--ease-in) both; }
::view-transition-new(root) { animation: vt-fade-in 0.3s var(--ease-out) both; }
@keyframes vt-fade-out { to { opacity: 0; } }
@keyframes vt-fade-in { from { opacity: 0; } }
.hero-image { view-transition-name: hero; } /* must be unique on the page */
@media (prefers-reduced-motion: reduce) { ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; } }
SPA では、DOM の更新を次のようにラップします:document.startViewTransition ? document.startViewTransition(update) : update();。サポートされていないブラウザーでは、アニメーションがスキップされます。
ステップ 4:スクロール連動アニメーション
animation-timeline を使用した CSS のみの実装
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.scroll-reveal { animation: reveal linear both; animation-timeline: view(); animation-range: entry 0% entry 100%; }
}
}
@keyframes reveal { from { opacity: 0; transform: translateY(40px); } to { opacity: 1; transform: translateY(0); } }
animation-timeline は animation の一括指定の後に記述する必要があります(この一括指定によって値がリセットされます)。スクロール連動アニメーションに対応していない環境でも、コンテンツは完全に表示されたままになります。
Intersection Observer を使った方法
/* Hide only when JS is running, so content is never lost without JS */
.js .reveal { opacity: 0; transform: translateY(30px); transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out); }
.js .reveal.is-visible { opacity: 1; transform: translateY(0); }
document.documentElement.classList.add('js');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) { entry.target.classList.add('is-visible'); observer.unobserve(entry.target); }
});
}, { threshold: 0.1 });
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
ステップ 5:ローディングアニメーション
スケルトンローダー
.skeleton { background: var(--gray-200); border-radius: var(--radius-md); position: relative; overflow: hidden; }
.skeleton::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(90deg, transparent, oklch(1 0 0 / 0.4), transparent);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
.skeleton-text { height: 1em; margin-bottom: 0.75em; }
.skeleton-text:last-child { width: 60%; }
.skeleton-avatar { width: 48px; height: 48px; border-radius: 50%; }
スピナー
.spinner { width: 24px; height: 24px; border: 3px solid var(--gray-200); border-top-color: var(--brand-500); border-radius: 50%; animation: spin 0.7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
3 点のバウンス
.dots-loader { display: flex; gap: 4px; }
.dots-loader span { width: 8px; height: 8px; background: var(--brand-500); border-radius: 50%; animation: dot-bounce 1.2s ease-in-out infinite; }
.dots-loader span:nth-child(2) { animation-delay: 0.15s; }
.dots-loader span:nth-child(3) { animation-delay: 0.3s; }
@keyframes dot-bounce { 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }
ステップ 6:CSS アニメーションのテクニック
トランジションとアニメーションの違い
transition:状態の変化(ホバー、フォーカス、クラスの切り替え)に使用します。A から B への変化です。animation:複数段階のシーケンス、継続的な効果、または開始時のアニメーションに使用します。
アニメーションのフィルモード
| 値 | 動作 |
|---|---|
| none | アニメーション後に元の状態に戻ります |
| forwards | 最後のキーフレームの状態を維持します |
| backwards | 遅延時間中に最初のキーフレームを適用します |
| both | forwards と backwards を組み合わせます。通常はこちらを使用します |
ステップ 7:CSS によるスプリング物理
:root {
--spring-gentle: cubic-bezier(0.34, 1.2, 0.64, 1); /* Slight overshoot */
--spring-medium: cubic-bezier(0.34, 1.56, 0.64, 1); /* Noticeable bounce */
--spring-strong: cubic-bezier(0.22, 1.8, 0.36, 1); /* Dramatic bounce */
--spring-snappy: cubic-bezier(0.075, 0.82, 0.165, 1); /* Not a spring: fast ease-out (easeOutCirc), no overshoot */
}
複数回バウンドさせるには、キーフレームを使用します。
@keyframes spring-bounce {
0% { transform: scale(0.8); } 40% { transform: scale(1.08); }
60% { transform: scale(0.97); } 80% { transform: scale(1.02); } 100% { transform: scale(1); }
}
ステップ 8:モーションの軽減
グローバルな動きの低減
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important; animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important; scroll-behavior: auto !important;
}
}
推奨される方法は、動きを伴わないトランジションに置き換えることです(例:トランスフォームではなく不透明度を変化させる)。上記のグローバルルールは安全策です。スピナーも1回転後に停止するため、ローダーには回転しない代替手段(不透明度の点滅や「読み込み中…」というテキスト)を用意してください。
JavaScript でアニメーションを制御する場合(Web Animations API、GSAP、Framer Motion)は、設定を確認し、変更を監視してください。
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
const duration = () => (reduce.matches ? 0 : 300);
reduce.addEventListener('change', () => { /* re-read duration(), stop autoplaying loops */ });
また、1秒あたり3回を超えて点滅させないでください(WCAG 2.3.1)。5秒を超えて続く自動再生の動きには、一時停止コントロールが必要です(WCAG 2.2.2)。
ステップ9:パフォーマンスの最適化
基本ルール
アニメーションの対象はtransformとopacityのみにしてください。これらは GPU で合成されるため、レイアウトやペイントを発生させません。
プロパティのコスト
| プロパティ | レイアウト | ペイント | 合成 | パフォーマンス |
|---|---|---|---|---|
| transform | なし | なし | はい | 優秀 |
| opacity | なし | なし | はい | 優秀 |
| filter | なし | 多くの場合 | 変動 | 良好(広い領域のぼかしはコストが高い) |
| box-shadow | なし | はい | なし | 中程度 |
| width/height | はい | はい | なし | 低い |
| top/left | はい | はい | なし | 低い |
will-changeは控えめに使用してください。アニメーション開始直前の要素にのみ設定し、終了後は削除します。多くの要素に永続的に設定しないでください。
ステップ10:リストのアニメーションを段階的に表示する
カスタムプロパティを使った動的な段階表示
.stagger-list > * {
animation: stagger-in 0.4s var(--ease-out) both;
animation-delay: calc(var(--index, 0) * 60ms);
}
@keyframes stagger-in { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
<ul class="stagger-list">
<li style="--index: 0">Item 1</li>
<li style="--index: 1">Item 2</li>
<li style="--index: 2">Item 3</li>
</ul>
段階表示のルール
- 段階表示の合計時間は600msを超えないようにします
- 項目ごとの遅延:項目間で40~80ms
- 8~10項目以降は遅延を上限に達させます(すべて同時に表示できます):
animation-delay: calc(min(var(--index, 0), 8) * 60ms);
クイックリファレンス
- すべてのアニメーションには、フィードバック、ガイダンス、状態変化のいずれかの目的が必要です。
- 再生時間は短くします。ほとんどの UI インタラクションでは100~400msです。
- 登場にはease-out、退出にはease-in、移動にはease-in-outを使用します。
- パフォーマンスを最大限に高めるには、アニメーションの対象を
transformとopacityのみにします。 - 必ず
prefers-reduced-motionを実装します。 - コンテンツの読み込みにはスケルトンローダー、操作にはスピナーを使用します。
- リストのアニメーションは40~80ms間隔で段階的に表示し、合計時間を600ms未満にします。
- 終了状態を維持するには
animation: ... bothを使用します。 - スプリングイージング(値が1を超える
cubic-bezier)を使うと、個性を加えられます。 - ローエンドデバイスでテストします。カクつくアニメーションは、アニメーションがない状態よりも劣ります。
動きを確認する
動きはコードだけでは評価できません。引き渡す前に確認してください。
- フレーム:Playwrightで、アニメーション中と停止時に要素をキャプチャします(例:アニメーションを開始した直後に
page.screenshot()を実行し、その後await page.waitForTimeout(duration + 50)を実行します)。Readで両方を確認します。開始状態と終了状態が正しく、要素が跳ねたりはみ出したりしないことを確認してください。 - 動きの低減:
await page.emulateMedia({ reducedMotion: 'reduce' })(またはbrowser.newContext({ reducedMotion: 'reduce' }))を指定して再実行し、コンテンツがすぐに表示され、静止していることを確認します。 - パフォーマンス:DevTools Performance(または
chrome://tracing)で、長いフレームやアニメーション中の「Layout」を確認します。発生している場合は、transform/opacityに切り替えてください。