interaction-design について
モーション、フィードバック、状態遷移を通じてUIインタラクションを設計するスキルです。UIデザイナーや開発者向けに、アクセシビリティとパフォーマンスを考慮したローディング表示やジェスチャーなどの実装方法を扱います。
主な機能
- 01マイクロインタラクションとフィードバックのパターン
- 02ページとコンポーネントの遷移
- 03ローディング状態と進捗インジケーター
- 04ジェスチャーとドラッグ&ドロップの指針
- 05動きを抑える設定とパフォーマンスの実践
活用シーン
- 01ボタンやコントロールへのフィードバック追加
- 02ローディングと進捗状態の設計
- 03スワイプやドラッグ&ドロップの実装
- 04アクセシブルなページ遷移の計画
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ wshobson/ agents --skill interaction-designインストール後、Claude Code で/interaction-designを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/animation-libraries.mdmicrointeraction-patterns.mdscroll-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 }}
/>
);
}
ベストプラクティス
- パフォーマンス優先: 滑らかな 60fps のために
transformとopacityを使用する - モーション低減のサポート: 常に
prefers-reduced-motionを尊重する - 一貫したタイミング: アプリ全体でタイミングスケールを使用する
- 自然な物理挙動: リニアよりもスプリングアニメーションを優先する
- 中断可能: ユーザーが長いアニメーションをキャンセルできるようにする
- プログレッシブエンハンスメント: JS アニメーションなしでも動作するようにする
- 実機でのテスト: パフォーマンスは大きく変動する
よくある問題
- カクつくアニメーション:
width、height、top、leftのアニメーションは避ける - アニメーションの過剰: モーションが多すぎると疲労を引き起こす
- インタラクションのブロック: アニメーション中にユーザー入力を決して妨げない
- メモリリーク: アンマウント時にアニメーションリスナーをクリーンアップする
- コンテンツのちらつき: 最適化のために
will-changeを控えめに使用する
コメント
Markdown 対応