Interaction Design by @wshobson · Skillet
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.
Just instructions. No commands, network, or file access.
Medium transitions (modals, page changes)
500ms+ Complex choreographed animations
3. Easing Functions
--ease-out : cubic-bezier (0.16 , 1 , 0.3 , 1 );
--ease-in : cubic-bezier (0.55 , 0 , 1 , 0.45 );
--ease-in-out : cubic-bezier (0.65 , 0 , 0.35 , 1 );
--spring : cubic-bezier (0.34 , 1.56 , 0.64 , 1 );
Quick Start: Button Microinteraction 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 >
);
}
Interaction Patterns
1. Loading States Skeleton Screens : Preserve layout while loading
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 >
);
}
Progress Indicators : Show determinate progress
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. State Transitions Toggle with smooth transition :
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. Page Transitions Framer Motion layout animations :
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. Feedback Patterns 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. Gesture Interactions 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 Animation Patterns
Keyframe Animations @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 Transitions .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 );
}
Accessibility Considerations
@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 }}
/>
);
}
Best Practices
Performance First : Use transform and opacity for smooth 60fps
Reduce Motion Support : Always respect prefers-reduced-motion
Consistent Timing : Use a timing scale across the app
Natural Physics : Prefer spring animations over linear
Interruptible : Allow users to cancel long animations
Progressive Enhancement : Work without JS animations
Test on Devices : Performance varies significantly
Common Issues
Janky Animations : Avoid animating width, height, top, left
Over-animation : Too much motion causes fatigue
Blocking Interactions : Never prevent user input during animations
Memory Leaks : Clean up animation listeners on unmount
Flash of Content : Use will-change sparingly for optimization
~2.0K tokens