Animation That Works: Making Interfaces Feel Alive
Most UI animation is decoration. The best animation is communication. Here's how I think about motion in product interfaces and what separates good animation from noise.
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 allRust • January 8, 2025
From Figma to Code: How I Close the Design–Engineering Gap
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.
Typescript • December 20, 2024
What Building Mobile Apps Taught Me About Constraints
Mobile development forces decisions that web development lets you defer. Working under those constraints changed how I approach every product I build.
Rust • February 10, 2025
Outcome Engineering: The Discipline of Building What Actually Matters
Most engineers build features. Outcome engineers build results. The distinction sounds small but changes everything about how you approach a problem.