
There's a version of UI animation that makes designers proud and users annoyed. You've seen it — the landing page where every element fades in as you scroll, each with a slightly different delay, creating a cascade of movement that takes three seconds to resolve before you can read anything. It looks great in a Dribbble shot. It's exhausting to experience.
Good animation is different. Good animation communicates. It answers questions the user didn't know they were asking: Where did that go? What just happened? What's coming next? When animation does that work, users don't notice it. They just feel oriented.
The Purpose Question
Before writing a single animation, ask: what is this communicating?
There are five things animation can usefully communicate in a UI:
- State change — something turned on, off, selected, or deselected
- Spatial relationship — a panel slid in from the right, so I can go back left
- Causality — I tapped this button and that response appeared
- Hierarchy — this element is more important than that one
- Progress — something is happening and hasn't finished yet
If your animation isn't doing one of these, it's decoration. Decoration has a cost — attention, performance, and sometimes accessibility. It needs to earn its place.
Duration and Easing
This is where most animation goes wrong. The defaults in most libraries are too slow and too bouncy for production interfaces.
My baselines:
| Interaction | Duration |
|---|---|
| Micro-interaction (hover, focus) | 100–150ms |
| Element entry / exit | 200–300ms |
| Page transition | 300–400ms |
| Modal / sheet open | 250–350ms |
Anything over 400ms will feel sluggish to most users. Anything under 100ms will feel instant — which is sometimes what you want, but usually not for visibility changes.
For easing, I default to ease-out for entries (quick start, graceful settle) and ease-in for exits (slow start, fast departure). The thing entering deserves attention; the thing leaving doesn't need it.
// Framer Motion — entry/exit pattern I use everywhere
const variants = {
hidden: { opacity: 0, y: 6 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.25, ease: 'easeOut' },
},
exit: {
opacity: 0,
y: -4,
transition: { duration: 0.15, ease: 'easeIn' },
},
};
<motion.div variants={variants} initial="hidden" animate="visible" exit="exit">
{children}
</motion.div>;Framer Motion in Practice
I use Framer Motion for most animation work in React. Its declarative model maps well to how I think about animation states. A few patterns I reach for constantly:
Layout animations
The magic trick. Add layout to a component and Framer Motion automatically animates between layout changes — items reordering in a list, a container expanding, a sidebar opening. This is the hardest animation to write manually and trivially easy with Framer.
{
items.map((item) => (
<motion.div key={item.id} layout>
{item.content}
</motion.div>
));
}AnimatePresence for exit animations
React removes elements from the DOM immediately. AnimatePresence keeps them alive long enough to play an exit animation, then removes them.
<AnimatePresence>
{isOpen && (
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.97 }}
>
<Modal />
</motion.div>
)}
</AnimatePresence>Stagger children
When a list of items appears, staggering their entry adds rhythm without feeling chaotic. Keep the stagger tight — 40–60ms between items is enough to read the cascade.
const container = {
visible: {
transition: { staggerChildren: 0.05 },
},
};
const item = {
hidden: { opacity: 0, y: 8 },
visible: { opacity: 1, y: 0 },
};CSS Transitions vs. JavaScript Animation
Not everything needs Framer Motion. For simple hover states, focus rings, and color transitions, CSS transition is faster, simpler, and composable with Tailwind:
/* Tailwind shorthand */
className="transition-colors duration-150 hover:bg-muted"I reach for JavaScript animation (Framer Motion) when I need:
- Exit animations
- Spring physics
- Gesture-driven animation (drag, scroll)
- Complex sequencing or orchestration
Everything else: CSS transitions.
Accessibility
prefers-reduced-motion is not optional.1 Users who have set this preference have told the OS they find motion uncomfortable. Ignoring it is actively harmful for some users.
In Tailwind:
className="transition-transform motion-reduce:transition-none"In Framer Motion:
import { useReducedMotion } from 'framer-motion';
function AnimatedComponent() {
const reduce = useReducedMotion();
return <motion.div animate={{ x: reduce ? 0 : 100 }} />;
}The best animation is the one you notice only when it's gone. If removing an animation makes an interface feel abrupt, broken, or disorienting — that animation was doing real work. Keep it. If removing it changes nothing, remove it.2
Footnotes
-
According to the Vestibular Disorders Association, approximately 35% of adults over 40 in the United States have experienced some form of vestibular dysfunction. For these users, parallax scrolling and constant motion can cause genuine physical discomfort including nausea and dizziness. ↩
-
This test — "what breaks when I remove it?" — is the clearest way I know to distinguish functional animation from decorative animation. Apply it ruthlessly before shipping. ↩
More essays
View all
AI Tooling • April 30, 2026
How Popular AI Systems Use RAG to Deliver Better Answers
A portfolio is just a product with one user. Here's what building mine taught me about design decisions, scope, and shipping things that aren't perfect.

System Design • April 23, 2026
How DNS Works Behind the Scenes of Every Web Request
The handoff between design and engineering is where quality goes to die. Here's the workflow I use to keep intent intact from the first frame to the final commit.

System Design • April 15, 2026
Understanding How SSO Actually Works Behind the Scenes
A honest look at the tools, libraries, and workflows that make up my daily design engineering practice — what works, what doesn't, and why.