mcpField
M
motion-design

motion-design

github.com/arnavpuriデザイン・UI17121 回インストール5 週間前に更新
リポジトリ

motion-design について

UIアニメーションやマイクロインタラクションを作るエージェント向けのモーションデザインスキルです。トランジション、スクロール演出、ローディングのパターンに加え、パフォーマンスとモーション軽減への対応を扱います。

主な機能

  1. 01アニメーション時間とイージングの指針
  2. 02ボタン、トグル、画面遷移のパターン
  3. 03スクロール連動とローディングのパターン
  4. 04モーション軽減への対応方法
  5. 05アニメーションの性能最適化と検証

活用シーン

  1. 01ボタンやトグルに操作フィードバックを追加
  2. 02画面表示やリストの段階的表示を設計
  3. 03ローディング用プレースホルダーや表示を作成
  4. 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 つのルール
  1. すべてのアニメーションには目的が必要です:注意を誘導する、フィードバックを提供する、関連性を示す、または状態の変化を伝えます。
  2. 速いほど、ほとんどの場合、優れています:UI アニメーションは 100~500ms にします。
  3. 変化するものをアニメーションさせます:実際に変化するプロパティだけをアニメーションさせます。
所要時間のガイドライン
操作所要時間
ボタンのホバー/押下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遅延時間中に最初のキーフレームを適用します
bothforwards と 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);

クイックリファレンス

  1. すべてのアニメーションには、フィードバック、ガイダンス、状態変化のいずれかの目的が必要です。
  2. 再生時間は短くします。ほとんどの UI インタラクションでは100~400msです。
  3. 登場にはease-out、退出にはease-in、移動にはease-in-outを使用します。
  4. パフォーマンスを最大限に高めるには、アニメーションの対象をtransformとopacityのみにします。
  5. 必ずprefers-reduced-motionを実装します。
  6. コンテンツの読み込みにはスケルトンローダー、操作にはスピナーを使用します。
  7. リストのアニメーションは40~80ms間隔で段階的に表示し、合計時間を600ms未満にします。
  8. 終了状態を維持するにはanimation: ... bothを使用します。
  9. スプリングイージング(値が1を超えるcubic-bezier)を使うと、個性を加えられます。
  10. ローエンドデバイスでテストします。カクつくアニメーションは、アニメーションがない状態よりも劣ります。

動きを確認する

動きはコードだけでは評価できません。引き渡す前に確認してください。

  1. フレーム:Playwrightで、アニメーション中と停止時に要素をキャプチャします(例:アニメーションを開始した直後にpage.screenshot()を実行し、その後await page.waitForTimeout(duration + 50)を実行します)。Readで両方を確認します。開始状態と終了状態が正しく、要素が跳ねたりはみ出したりしないことを確認してください。
  2. 動きの低減:await page.emulateMedia({ reducedMotion: 'reduce' })(またはbrowser.newContext({ reducedMotion: 'reduce' }))を指定して再実行し、コンテンツがすぐに表示され、静止していることを確認します。
  3. パフォーマンス:DevTools Performance(またはchrome://tracing)で、長いフレームやアニメーション中の「Layout」を確認します。発生している場合は、transform/opacityに切り替えてください。

コメント