mcpField
I
interaction-design

interaction-design

github.com/wshobsonデザイン・UI41.9k16.4k 回インストール2 週間前に更新
リポジトリ

interaction-design について

モーション、フィードバック、状態遷移を通じてUIインタラクションを設計するスキルです。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/wshobson/agents --skill interaction-design

インストール後、Claude Code で/interaction-designを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
animation-libraries.md
microinteraction-patterns.md
scroll-animations.md

詳細

インタラクションデザイン

モーション、フィードバック、そして配慮された状態遷移を通じて、使いやすさを高めユーザーを喜ばせる、魅力的で直感的なインタラクションを作成します。

このスキルを使う場面

  • ユーザーフィードバックを強化するマイクロインタラクションの追加
  • スムーズなページおよびコンポーネントの遷移の実装
  • ローディング状態とスケルトン画面の設計
  • ジェスチャーベースのインタラクションの作成
  • 通知およびトーストシステムの構築
  • ドラッグ&ドロップインターフェースの実装
  • スクロールトリガーアニメーションの追加
  • ホバー状態とフォーカス状態の設計

基本原則

1. 目的のあるモーション

モーションは装飾ではなく、伝達するものであるべきです:

  • フィードバック: ユーザーの操作が行われたことを確認する
  • 方向性: 要素がどこから来てどこへ行くのかを示す
  • フォーカス: 重要な変化に注意を向けさせる
  • 連続性: 遷移中もコンテキストを維持する
2. タイミングのガイドライン
持続時間用途
100-150msマイクロフィードバック(ホバー、クリック)
200-300ms小さな遷移(トグル、ドロップダウン)
300-500ms中程度の遷移(モーダル、ページ変更)
500ms+複雑に構成されたアニメーション
3. イージング関数
/* Common easings */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* Decelerate - entering */
--ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* Accelerate - exiting */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* Both - moving between */
--spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* Overshoot - playful */

クイックスタート: ボタンのマイクロインタラクション

import { motion } from "framer-motion";

export function InteractiveButton({ children, onClick }) {
  return (
    <motion.button
      onClick={onClick}
      whileHover={{ scale: 1.02 }}
      whileTap={{ scale: 0.98 }}
      transition={{ type: "spring", stiffness: 400, damping: 17 }}
      className="px-4 py-2 bg-blue-600 text-white rounded-lg"
    >
      {children}
    </motion.button>
  );
}

インタラクションパターン

1. ローディング状態

スケルトン画面: 読み込み中もレイアウトを維持する

function CardSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="h-48 bg-gray-200 rounded-lg" />
      <div className="mt-4 h-4 bg-gray-200 rounded w-3/4" />
      <div className="mt-2 h-4 bg-gray-200 rounded w-1/2" />
    </div>
  );
}

プログレスインジケーター: 確定した進捗を表示する

function ProgressBar({ progress }: { progress: number }) {
  return (
    <div className="h-2 bg-gray-200 rounded-full overflow-hidden">
      <motion.div
        className="h-full bg-blue-600"
        initial={{ width: 0 }}
        animate={{ width: `${progress}%` }}
        transition={{ ease: "easeOut" }}
      />
    </div>
  );
}
2. 状態遷移

スムーズな遷移を伴うトグル:

function Toggle({ checked, onChange }) {
  return (
    <button
      role="switch"
      aria-checked={checked}
      onClick={() => onChange(!checked)}
      className={`
        relative w-12 h-6 rounded-full transition-colors duration-200
        ${checked ? "bg-blue-600" : "bg-gray-300"}
      `}
    >
      <motion.span
        className="absolute top-1 left-1 w-4 h-4 bg-white rounded-full shadow"
        animate={{ x: checked ? 24 : 0 }}
        transition={{ type: "spring", stiffness: 500, damping: 30 }}
      />
    </button>
  );
}
3. ページ遷移

Framer Motion のレイアウトアニメーション:

import { AnimatePresence, motion } from "framer-motion";

function PageTransition({ children, key }) {
  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={key}
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -20 }}
        transition={{ duration: 0.3 }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  );
}
4. フィードバックパターン

クリック時のリップルエフェクト:

function RippleButton({ children, onClick }) {
  const [ripples, setRipples] = useState([]);

  const handleClick = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const ripple = {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top,
      id: Date.now(),
    };
    setRipples((prev) => [...prev, ripple]);
    setTimeout(() => {
      setRipples((prev) => prev.filter((r) => r.id !== ripple.id));
    }, 600);
    onClick?.(e);
  };

  return (
    <button onClick={handleClick} className="relative overflow-hidden">
      {children}
      {ripples.map((ripple) => (
        <span
          key={ripple.id}
          className="absolute bg-white/30 rounded-full animate-ripple"
          style={{ left: ripple.x, top: ripple.y }}
        />
      ))}
    </button>
  );
}
5. ジェスチャーインタラクション

スワイプで閉じる:

function SwipeCard({ children, onDismiss }) {
  return (
    <motion.div
      drag="x"
      dragConstraints={{ left: 0, right: 0 }}
      onDragEnd={(_, info) => {
        if (Math.abs(info.offset.x) > 100) {
          onDismiss();
        }
      }}
      className="cursor-grab active:cursor-grabbing"
    >
      {children}
    </motion.div>
  );
}

CSS アニメーションパターン

キーフレームアニメーション
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

.animate-fadeIn {
  animation: fadeIn 0.3s ease-out;
}
.animate-pulse {
  animation: pulse 2s ease-in-out infinite;
}
.animate-spin {
  animation: spin 1s linear infinite;
}
CSS トランジション
.card {
  transition:
    transform 0.2s ease-out,
    box-shadow 0.2s ease-out;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}

アクセシビリティに関する考慮事項

/* Respect user motion preferences */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
function AnimatedComponent() {
  const prefersReducedMotion = window.matchMedia(
    "(prefers-reduced-motion: reduce)",
  ).matches;

  return (
    <motion.div
      animate={{ opacity: 1 }}
      transition={{ duration: prefersReducedMotion ? 0 : 0.3 }}
    />
  );
}

ベストプラクティス

  1. パフォーマンス優先: 滑らかな 60fps のために transform と opacity を使用する
  2. モーション低減のサポート: 常に prefers-reduced-motion を尊重する
  3. 一貫したタイミング: アプリ全体でタイミングスケールを使用する
  4. 自然な物理挙動: リニアよりもスプリングアニメーションを優先する
  5. 中断可能: ユーザーが長いアニメーションをキャンセルできるようにする
  6. プログレッシブエンハンスメント: JS アニメーションなしでも動作するようにする
  7. 実機でのテスト: パフォーマンスは大きく変動する

よくある問題

  • カクつくアニメーション: width、height、top、left のアニメーションは避ける
  • アニメーションの過剰: モーションが多すぎると疲労を引き起こす
  • インタラクションのブロック: アニメーション中にユーザー入力を決して妨げない
  • メモリリーク: アンマウント時にアニメーションリスナーをクリーンアップする
  • コンテンツのちらつき: 最適化のために will-change を控えめに使用する

コメント