Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Design and implement microinteractions, motion design, transitions, and user feedback patterns. Use when adding polish to UI interactions, implementing loading states, or creating delightful user experiences.
.claude/skills/itamarzand88-interaction-design/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 120% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 116% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 112% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 100% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 93% | 0% |
<!-- source: wshobson-interaction-design — https://raw.githubusercontent.com/wshobson/agents/main/plugins/ui-design/skills/interaction-design/SKILL.md -->
Create engaging, intuitive interactions through motion, feedback, and thoughtful state transitions that enhance usability and delight users.
Motion should communicate, not decorate:
| Duration | Use Case | | --------- | ----------------------------------------- | | 100-150ms | Micro-feedback (hovers, clicks) | | 200-300ms | Small transitions (toggles, dropdowns) | | 300-500ms | Medium transitions (modals, page changes) | | 500ms+ | Complex choreographed animations |
css/* 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 */
tsximport { 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> ); }
Skeleton Screens: Preserve layout while loading
tsxfunction 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> ); }
Progress Indicators: Show determinate progress
tsxfunction 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> ); }
Toggle with smooth transition:
tsxfunction 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> ); }
Framer Motion layout animations:
tsximport { 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> ); }
Ripple effect on click:
tsxfunction 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> ); }
Swipe to dismiss:
tsxfunction 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); }
css/* 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; } }
tsxfunction AnimatedComponent() { const prefersReducedMotion = window.matchMedia( "(prefers-reduced-motion: reduce)", ).matches; return ( <motion.div animate={{ opacity: 1 }} transition={{ duration: prefersReducedMotion ? 0 : 0.3 }} /> ); }
transform and opacity for smooth 60fpsprefers-reduced-motionwidth, height, top, leftwill-change sparingly for optimization| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 7,157 | 3,602 | -50% | 1 | 1 | 0% | 1,601 | 3,104 | +94% | 0 | 0 | — |
case-02 | pass→pass | 7,680 | 5,494 | -28% | 1 | 1 | 0% | 1,587 | 3,257 | +105% | 0 | 0 | — |
case-03 | fail→pass | 7,375 | 4,582 | -38% | 1 | 1 | 0% | 1,505 | 3,317 | +120% | 0 | 0 | — |
case-04 | fail→pass | 8,232 | 9,304 | +13% | 1 | 1 | 0% | 1,996 | 4,314 | +116% | 0 | 0 | — |
case-05 | fail→pass | 7,479 | 4,505 | -40% | 1 | 1 | 0% | 1,636 | 3,474 | +112% | 0 | 0 | — |
case-06 | pass→pass | 7,603 | 6,973 | -8% | 1 | 1 | 0% | 1,837 | 3,754 | +104% | 0 | 0 | — |
case-07 | fail→pass | 8,333 | 4,618 | -45% | 1 | 1 | 0% | 1,689 | 3,373 | +100% | 0 | 0 | — |
case-08 | pass→pass | 9,591 | 11,093 | +16% | 1 | 1 | 0% | 1,871 | 4,965 | +165% | 0 | 0 | — |
case-09 | pass→pass | 6,120 | 3,311 | -46% | 1 | 1 | 0% | 1,464 | 2,941 | +101% | 0 | 0 | — |
case-10 | fail→pass | 8,089 | 6,791 | -16% | 1 | 1 | 0% | 1,909 | 3,691 | +93% | 0 | 0 | — |
case-11 | pass→pass | 4,652 | 4,702 | +1% | 1 | 1 | 0% | 947 | 3,188 | +237% | 0 | 0 | — |
case-12 | pass→pass | 10,455 | 9,837 | -6% | 1 | 1 | 0% | 2,279 | 4,556 | +100% | 0 | 0 | — |
case-13 | pass→pass | 3,220 | 3,752 | +17% | 1 | 1 | 0% | 643 | 3,066 | +377% | 0 | 0 | — |
case-14 | pass→pass | 7,280 | 4,150 | -43% | 1 | 1 | 0% | 1,307 | 3,158 | +142% | 0 | 0 | — |
case-15 | pass→pass | 5,895 | 5,881 | -0% | 1 | 1 | 0% | 1,326 | 3,537 | +167% | 0 | 0 | — |
case-16 | pass→pass | 7,756 | 7,913 | +2% | 1 | 1 | 0% | 1,682 | 4,057 | +141% | 0 | 0 | — |
case-17 | fail→fail | 6,494 | 4,105 | -37% | 1 | 1 | 0% | 1,395 | 3,188 | +129% | 0 | 0 | — |
case-18 | pass→pass | 3,372 | 3,805 | +13% | 1 | 1 | 0% | 666 | 2,973 | +346% | 0 | 0 | — |
case-19 | pass→pass | 7,010 | 4,970 | -29% | 1 | 1 | 0% | 1,699 | 3,304 | +94% | 0 | 0 | — |
case-20 | pass→pass | 4,097 | 4,149 | +1% | 1 | 1 | 0% | 799 | 3,185 | +299% | 0 | 0 | — |
case-21 | pass→pass | 6,790 | 5,349 | -21% | 1 | 1 | 0% | 1,474 | 3,222 | +119% | 0 | 0 | — |
case-22 | pass→pass | 2,741 | 4,233 | +54% | 1 | 1 | 0% | 574 | 3,167 | +452% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.