Creating UI Element Microanimations
Microanimations are visual feedback at the individual element level: pressed button, loading list, switched toggle, notification appeared. They make the interface "alive" and reduce cognitive load: users see their action registered.
Principles
Duration. Microanimations — 100–300ms. Slower — irritating. Faster — imperceptible. Exception: animations with physical meaning (spring effect, inertia) can be longer.
Easing. ease-out for appearances (quick start, smooth deceleration — mimics real objects). ease-in for disappearances. spring for interactive elements.
Don't animate what didn't change. If user clicks a button and nothing changes logically — animation lies.
Buttons
The most common microanimation. Three levels:
CSS-only (hover + active):
.btn {
transition: transform 120ms ease-out, box-shadow 120ms ease-out;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
}
.btn:active {
transform: translateY(0) scale(0.98);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
Loading state (button with spinner):
const Button: React.FC<ButtonProps> = ({ children, onClick, loading }) => {
return (
<button
onClick={onClick}
disabled={loading}
className={`btn ${loading ? 'btn--loading' : ''}`}
>
<AnimatePresence mode="wait">
{loading ? (
<motion.span
key="spinner"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.5 }}
transition={{ duration: 0.15 }}
>
<SpinnerIcon />
</motion.span>
) : (
<motion.span
key="label"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{children}
</motion.span>
)}
</AnimatePresence>
</button>
);
};
Success state (button → checkmark → "Saved" text):
type BtnState = 'idle' | 'loading' | 'success';
const SaveButton: React.FC = () => {
const [state, setState] = useState<BtnState>('idle');
const handleClick = async () => {
setState('loading');
await saveData();
setState('success');
setTimeout(() => setState('idle'), 2000);
};
return (
<motion.button
onClick={handleClick}
animate={state === 'success' ? { backgroundColor: '#22c55e' } : {}}
transition={{ duration: 0.3 }}
>
{state === 'idle' && 'Save'}
{state === 'loading' && <Spinner />}
{state === 'success' && <><CheckIcon /> Saved</>}
</motion.button>
);
};
Form: Error and Success Appearance
// Field with animated error
const FormField: React.FC<{ error?: string }> = ({ error, ...props }) => (
<div className="field">
<input {...props} className={error ? 'input--error' : ''} />
<AnimatePresence>
{error && (
<motion.p
key="error"
initial={{ opacity: 0, y: -4, height: 0 }}
animate={{ opacity: 1, y: 0, height: 'auto' }}
exit={{ opacity: 0, y: -4, height: 0 }}
transition={{ duration: 0.2 }}
className="field__error"
>
{error}
</motion.p>
)}
</AnimatePresence>
</div>
);
Shake animation on invalid submit:
const shakeVariants = {
idle: { x: 0 },
shake: { x: [0, -10, 10, -8, 8, -4, 4, 0] },
};
<motion.form
variants={shakeVariants}
animate={hasErrors ? 'shake' : 'idle'}
transition={{ duration: 0.5, ease: 'easeInOut' }}
>
Toggle / Checkbox
const Toggle: React.FC<{ checked: boolean; onChange: () => void }> = ({ checked, onChange }) => (
<button
role="switch"
aria-checked={checked}
onClick={onChange}
className="toggle"
>
<motion.div
className="toggle__thumb"
animate={{ x: checked ? 20 : 0 }}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
/>
</button>
);
Spring transition (stiffness: 500, damping: 30) gives physically believable motion with slight bounce.
List: Element Appearance
const listVariants = {
hidden: {},
visible: { transition: { staggerChildren: 0.05 } },
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.3 } },
};
const AnimatedList: React.FC<{ items: Item[] }> = ({ items }) => (
<motion.ul
variants={listVariants}
initial="hidden"
animate="visible"
>
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
<ItemCard item={item} />
</motion.li>
))}
</motion.ul>
);
staggerChildren: 0.05 — each next element appears with 50ms delay. For 20 items — entire list unfolds in 1 second, perceived as smooth cascade.
Toast Notifications
const ToastNotification: React.FC<{ message: string; type: 'success' | 'error' }> = ({ message, type }) => (
<motion.div
className={`toast toast--${type}`}
initial={{ opacity: 0, y: 50, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95, transition: { duration: 0.15 } }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
>
{type === 'success' ? <CheckCircleIcon /> : <AlertIcon />}
<span>{message}</span>
</motion.div>
);
Toast appears bottom-to-top with spring effect and exits fast (duration: 0.15) — important, exit should be faster than entry.
Animated Number Increment
For counters, statistics, cart:
const AnimatedNumber: React.FC<{ value: number }> = ({ value }) => {
const spring = useSpring(value, { stiffness: 100, damping: 30 });
const display = useTransform(spring, Math.round);
return <motion.span>{display}</motion.span>;
};
Number "spins" to new value physically smoothly.
Cursor Effects (Spotlight, Magnetic)
// Magnetic button — moves toward cursor
const MagneticButton: React.FC<ButtonProps> = ({ children }) => {
const ref = useRef<HTMLButtonElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const handleMouseMove = (e: React.MouseEvent) => {
const rect = ref.current!.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
x.set((e.clientX - centerX) * 0.3);
y.set((e.clientY - centerY) * 0.3);
};
const handleMouseLeave = () => { x.set(0); y.set(0); };
return (
<motion.button
ref={ref}
style={{ x, y }}
transition={{ type: 'spring', stiffness: 200, damping: 15 }}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
{children}
</motion.button>
);
};
Performance
You can only animate transform and opacity — these properties don't cause reflow. Animating width, height, top, left hurts FPS:
/* Bad */
.card { transition: width 300ms; }
/* Good */
.card { transition: transform 300ms; }
.card:hover { transform: scaleX(1.05); }
In framer-motion layout prop automatically animates size changes via FLIP technique (measures final state first, then animates transform from start to end).
Timeline
| Task | Time |
|---|---|
| Buttons (hover, active, loading, success) | 0.5 day |
| Forms (validation, shake, success) | 0.5 day |
| List stagger + toast | 0.5 day |
| Toggle, checkboxes, animated numbers | 0.5 day |
| Advanced effects (magnetic, spotlight) | 1 day |







