Loading...
Loading...
Expert micro-interaction architect for mobile apps, web applications, and responsive websites. Use this skill when the user asks to add, build, fix, audit, or consult on micro-interactions, animations, transitions, motion design, gesture feedback, haptics, loading states, skeleton screens, pull-to-refresh, swipe actions, scroll animations, button states, form validation feedback, toast notifications, modals, dropdowns, toggles, progress indicators, shared element transitions, spring physics, easing curves, motion tokens, or any interaction that provides visual/haptic/auditory feedback to user actions. Triggers on: "micro-interaction", "animation", "transition", "motion", "easing", "spring", "gesture", "haptic", "feedback", "loading state", "skeleton", "shimmer", "pull to refresh", "swipe", "drag", "hover effect", "press state", "focus ring", "scroll animation", "parallax", "stagger", "orchestration", "reduced motion", "View Transitions", "layout animation", "shared element", "hero animation", "morphing", "Framer Motion", "GSAP", "Lottie", "Rive", "React Spring", "anime.js", or any request to make an interface "feel better", "feel alive", "feel snappy", "feel responsive", or "feel polished".
npx skill4agent add solinkz/micro-interactions-skill micro-interactionsTRIGGER → RULES → FEEDBACK → LOOPS & MODES1. Does this action need confirmation? → ADD feedback animation
2. Is something loading or processing? → ADD progress/skeleton/shimmer
3. Is there a state change? → ANIMATE the transition
4. Could the user miss something important? → ADD attention-drawing motion
5. Is this a frequent/repeated action? → BE SUBTLE — don't annoy
6. Is this purely decorative? → SKIP unless brand demands it
7. Does it work without animation? → GOOD — animation enhances, never required
8. Would this frustrate on the 100th use? → TONE IT DOWN or removetransition@keyframesanimation-timeline@starting-style@media (pointer: fine/coarse)@media (hover: hover/none).spring(response: 0.35, dampingFraction: 0.8).snappy.bouncy.smooth.interactiveSpringUIImpactFeedbackGeneratorUINotificationFeedbackGeneratorUISelectionFeedbackGeneratorUIAccessibility.isReduceMotionEnabledanimateXAsStateAnimatedVisibilityAnimatedContentCrossfadespring(dampingRatio, stiffness)Spring.DampingRatioMediumBouncySpring.StiffnessMediumHapticFeedbackConstantsVibrationEffectSharedTransitionLayoutsharedElement()sharedBounds()useSharedValueuseAnimatedStylewithSpringwithTimingGestureDetectorGesture.Pan()Gesture.Pinch()lottie-react-nativeexpo-hapticsreact-native-haptic-feedback| Semantic | Duration | Use Case |
|---|---|---|
| Instant | 0-50ms | Ripple start, color feedback |
| Ultra-fast | 50-100ms | Checkbox, radio, small state change |
| Fast | 100-200ms | Button press, toggle, tooltip appear |
| Normal | 200-300ms | Panel expand, dropdown, modal open |
| Slow | 300-500ms | Page transition, complex layout shift |
| Dramatic | 500-1000ms | Onboarding, celebration, staggered list |
/* Standard (Material-like) */
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
--ease-decel: cubic-bezier(0, 0, 0.2, 1); /* entering screen */
--ease-accel: cubic-bezier(0.4, 0, 1, 1); /* leaving screen */
--ease-emphasized: cubic-bezier(0.05, 0.7, 0.1, 1); /* attention-drawing */
/* Expressive */
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* overshoot bounce */
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); /* fast decel */
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); /* smooth decel */
--ease-out-back: cubic-bezier(0.34, 1.3, 0.7, 1); /* slight overshoot */| Feel | Stiffness | Damping | Settle Time | Use Case |
|---|---|---|---|---|
| Gentle | 100-150 | 15-20 | ~500ms | Page transitions, modals |
| Default | 200-250 | 20-25 | ~350ms | General purpose, buttons |
| Snappy | 350-500 | 25-30 | ~200ms | Toggles, tabs, quick actions |
| Bouncy | 300-400 | 8-12 | ~600ms | Celebrations, playful UI |
| Stiff | 500+ | 30+ | ~150ms | Cursor following, direct manipulation |
Default → Hover: translateY(-1px), shadow increase, 150ms ease-out
→ Active: scale(0.97), shadow decrease, 80ms ease-out
→ Focus-visible: 2px outline, 2px offset, brand color
→ Loading: text fades, spinner appears, pointer-events: none
→ Success: background morphs green, text → checkmark, 200ms
→ Disabled: opacity 0.5, cursor not-allowed, no hoverTap → Thumb slides with spring (stiffness: 500, damping: 30)
→ Track color transitions 200ms ease
→ On mobile: haptic "nudge" at completion
→ Label text cross-fades if changingOverscroll → Progress indicator scales/rotates proportional to pull distance
Threshold → Haptic tick, indicator snaps to loading state
Loading → Spinner animation, content locked
Complete → Spinner morphs to checkmark, content slides back with springDrag start → Background color reveals behind item
Threshold 1 → Icon appears with scale-in, haptic tick
Threshold 2 → Full action area, stronger haptic
Release → If past threshold: item slides out, list collapses with spring
If before threshold: item springs back to originOpen → Sheet slides up with spring, scrim fades in 200ms
Drag → Sheet follows finger, velocity tracked
Release → Snap to nearest detent based on position + velocity
Dismiss → Sheet slides down with accelerate easing, scrim fades outTyping → No validation (never interrupt the user)
On blur → Validate
Valid → Green border + checkmark fade-in, 150ms ease-out
Invalid → Red border + shake (3 cycles, 4px, 400ms) + error slides down
Fixing error → Error cross-fades to success on next valid blur
Submit fail → Scroll to first error + pulse animation on error fieldPage load → Show skeletons matching exact content layout
→ Shimmer: gradient sweep left-to-right, 1.5s ease-in-out infinite
→ Content ready: skeleton cross-fades to real content, 200ms
→ Stagger: each skeleton block fades out 30ms apartAppear → Slide from bottom/top + scale 0.95→1.0 with spring (400ms)
Stack → Existing toasts compress with scale/translate
Dismiss → Swipe: follows finger, velocity-based dismiss
→ Auto: fade out + slide, 300ms ease-in
→ ARIA: role="status", aria-live="polite"Open → Scrim fades in (200ms) + content scales 0.95→1.0 with spring (300ms)
→ Focus trapped inside, first focusable element focused
Close → Content scales to 0.97 + fades (200ms) + scrim fades (150ms)
→ Focus returns to trigger element
→ Escape key, scrim click both closeOpen → Scale from 0.95 + opacity, transform-origin at trigger, 200ms ease-out
→ Items stagger in: 30ms delay each, translateY(-8px) → 0
Close → Reverse at 150ms (faster exit)
→ On click outside, Escape, or selectionEnter → translateY(-4px), shadow expands, 200ms ease-out
→ Image zoom 1.05x (if image card)
Leave → Return to rest, 250ms ease-out (slightly slower for smoothness)
Active → scale(0.98), shadow contracts, 100msEnter viewport → Fade + translateY(20px→0), 500ms ease-out
→ Stagger children by 50ms
→ Use IntersectionObserver (threshold: 0.1-0.2)
→ Fire once only (unobserve after trigger)Navigate → Source element morphs to destination position/size
→ Use View Transitions API (web) or SharedTransitionLayout (Android)
→ Cross-fade surrounding content
→ Duration: 300-400ms with emphasized easing
→ Non-shared content fades at 200msSelect → Active indicator slides to new position with spring
→ Content cross-fades (150ms) or slides in direction of selection
→ Old content fades/slides out simultaneously
→ Duration: 250ms, spring(stiffness: 400, damping: 28)Expand → Height animates from 0 (use grid row trick or max-height)
→ Chevron rotates 180° or 90° with same timing
→ 250ms ease-out
Collapse → Reverse at 200ms (faster)
→ Content clips with overflow: hidden during animationDeterminate → Bar width transitions, 400ms ease-out per update
→ Color can shift as progress increases (gray→blue→green)
Indeterminate → Sliding bar or rotating spinner
→ Bar: translateX(-100% → 400%), 1.5s ease-in-out infinite
Step-based → Completed step: number morphs to checkmark
→ Active step: pulse or glow animation
→ Line between steps fills with color sweepToggle → CSS custom properties transition 300ms ease
→ Sun/moon icon morphs (rotation + scale + crossfade)
→ Optional: circular clip-path reveal from toggle position (500ms)
→ Persist in localStorage, apply before paint (no flash)New item → Badge scales from 0 → 1.2 → 1.0 with spring
Count up → Number rolls (old slides up, new slides in from below)
Clear → Badge scales to 0, 200ms ease-in
Pulse → Subtle scale pulse 1.0 → 1.1 → 1.0, 2s infinite (optional, for urgency)Pickup → scale(1.05), shadow elevation increases, opacity(0.9), 100ms
→ Haptic on mobile
Dragging → Item follows cursor/finger, slight rotation (2-3°)
→ Drop targets highlight with border animation
→ Other items slide aside with spring to make space
Drop → Spring to final position (stiffness: 500, damping: 30)
→ Shadow returns to normal, scale to 1.0
Cancel → Spring back to origin with overshoot (300ms)Click → Button text fades to spinner (150ms)
Success → Spinner morphs to checkmark
→ Product thumbnail flies to cart icon (arc path, 500ms)
→ Cart icon bounces (scale 1.0→1.3→1.0, spring)
→ Badge count rolls up
→ Button text returns after 2sEach keystroke → Bar width transitions smoothly, 200ms ease-out
Weak (<6) → 25% fill, red
Fair (6-8) → 50% fill, orange (color cross-fades)
Good (8-12) → 75% fill, yellow→green
Strong (12+) → 100% fill, deep green + subtle pulse⌘+K open → Overlay fades (150ms), search box scales 0.95→1.0 with spring
Typing → Results filter with crossfade (no layout jump)
Arrow nav → Highlight slides smoothly between items
Enter → Action executes, palette scales down + fades (100ms)
Escape → Scale to 0.95 + fade out (100ms)transformopacityfilterclip-pathwidthheighttopleftmarginpaddingborderfont-sizebackground-color.will-animate {
will-change: transform, opacity; /* Apply before animation, remove after */
contain: layout style paint; /* Limit repaint scope */
}will-changeIntersectionObserver/* Approach 1: Opt-in animations (RECOMMENDED) */
/* Default: no animation. Add only when user allows */
.element { opacity: 1; transform: none; }
@media (prefers-reduced-motion: no-preference) {
.element {
animation: fadeIn 0.5s ease-out;
}
}
/* Approach 2: Disable for reduced-motion users */
@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;
}
}
/* Approach 3: Replace with non-motion alternative */
@media (prefers-reduced-motion: reduce) {
.notification {
animation: fadeIn 0.1ms ease-out; /* Instant fade, no slide */
}
}const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// React hook
function useReducedMotion() {
const [reduced, setReduced] = useState(false);
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
setReduced(mq.matches);
const handler = (e) => setReduced(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return reduced;
}<div aria-live="polite" aria-atomic="true"><!-- Toast announcements --></div>
<div role="alert"><!-- Error messages --></div>
<div role="progressbar" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">
<button aria-busy="true" aria-disabled="true"><!-- Loading button --></button>role="status"aria-live="polite"/* Mouse/trackpad — enable hover effects */
@media (pointer: fine) and (hover: hover) {
.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
.link:hover::after { transform: scaleX(1); }
}
/* Touch — use active/tap states */
@media (pointer: coarse) and (hover: none) {
.card:active { transform: scale(0.97); }
.interactive { -webkit-tap-highlight-color: transparent; }
}Desktop (>1024px): Full animations, hover effects, parallax, staggered reveals
Tablet (768-1024): Simplified animations, reduced parallax, larger targets
Mobile (<768px): Minimal animations (100-200ms), no hover, tap feedback only,
gestures (swipe, pull-to-refresh), haptic feedback{
"motion": {
"duration": {
"instant": "0ms",
"fast": "100ms",
"normal": "200ms",
"slow": "300ms",
"slower": "400ms",
"complex": "500ms"
},
"easing": {
"standard": "cubic-bezier(0.2, 0, 0, 1)",
"enter": "cubic-bezier(0, 0, 0.2, 1)",
"exit": "cubic-bezier(0.4, 0, 1, 1)",
"spring": "cubic-bezier(0.34, 1.56, 0.64, 1)",
"bounce": "cubic-bezier(0.68, -0.55, 0.265, 1.55)",
"emphasized": "cubic-bezier(0.05, 0.7, 0.1, 1)"
},
"spring": {
"gentle": { "stiffness": 120, "damping": 14 },
"default": { "stiffness": 200, "damping": 20 },
"snappy": { "stiffness": 400, "damping": 25 },
"bouncy": { "stiffness": 300, "damping": 10 }
},
"stagger": {
"fast": "30ms",
"normal": "50ms",
"slow": "80ms"
}
}
}| Dimension | Check | Weight |
|---|---|---|
| Feedback | Does every interactive element give immediate feedback? | Critical |
| Timing | Are durations in the 100-400ms sweet spot? | High |
| Easing | Are custom curves used (not linear or default ease)? | High |
| Consistency | Same action = same feedback everywhere? | High |
| Performance | Compositor-only properties? 60fps? | Critical |
| Accessibility | prefers-reduced-motion respected? ARIA live regions? | Critical |
| Responsive | Touch vs mouse adaptation? Breakpoint-appropriate? | High |
| Purpose | Does every animation serve a function? | Medium |
| Delight | Any moments of unexpected polish? | Medium |
| Restraint | Anything gratuitous that should be removed? | Medium |
Can CSS transition do it? → Use transition
Need keyframes / sequencing? → Use @keyframes
Need scroll-driven? → Use animation-timeline: scroll()/view()
Need entry from display:none? → Use @starting-style
Need layout/position animation? → Use Framer Motion layout / FLIP technique
Need spring physics? → Use Framer Motion / React Spring / Motion One
Need complex orchestration? → Use GSAP timeline
Need pre-built animation asset? → Use Lottie or Rive