The most downloaded command menu on the web has no entrance animation. That's not an oversight.
cmdk is used in Linear, Vercel, Raycast. It opens hundreds of times a day. The decision to ship it with no animation on enter, no animation on exit, is the best design decision in the component.
If you animate something that happens 200 times a day, you don't create delight. You create friction that compounds.
The frequency test
Before any animation, one question: how many times per day does a typical user trigger this?
The morphing feedback button that feels magical the first time becomes irritating by Thursday. The animation hasn't changed — the novelty has expired, and now you're paying the duration tax on every single interaction.
Keyboard actions — the hardest rule to follow
Never animate keyboard-initiated actions. No exceptions.
When you press a key, you expect an instant response. The disconnect between peripheral input and screen output means animation doesn't add physical value — it only creates latency.
Arrow keys in a list. Cmd+Tab. Any keyboard shortcut. If it starts with a keypress, the response must feel immediate.
macOS App Switcher — no animation. Cmd+Tab multiple times per second, across thousands of daily sessions. A 200ms transition would feel broken from day one.
Here's the pattern:
/* With animation — feels slow on keyboard */
.list-item {
transition: background 150ms ease-out;
}
/* Without — correct for keyboard navigation */
.list-item {
background: transparent;
/* No transition. The speed IS the feedback. */
}
// Detect if the interaction is keyboard-initiated
// and skip animation entirely
let isKeyboardNav = false;
document.addEventListener('keydown', () => {
isKeyboardNav = true;
});
document.addEventListener('mousedown', () => {
isKeyboardNav = false;
});
function highlightItem(el: HTMLElement) {
if (isKeyboardNav) {
// Instant — no class that triggers a transition
el.setAttribute('data-active', 'true');
} else {
// Allow transition only for mouse interactions
el.classList.add('is-transitioning');
el.setAttribute('data-active', 'true');
}
}
isKeyboardNav is true — arrow keys, Cmd+Tab, any shortcut.The 300ms wall
Every UI animation has a hard limit: 300ms.
Past 300ms, the user stops perceiving motion as feedback and starts perceiving it as delay.
The duration table for reference:
| Element | Duration |
|---|---|
| Button press, scale | 100–150ms |
| Tooltips, dropdowns | 150–250ms |
| Modals, drawers | 200–300ms |
| Page transitions | 250–350ms |
Larger elements animate slower. Exits are 20% faster than entrances. The exit doesn't need to be noticed — it just needs to not be abrupt.
:root {
--duration-fast: 150ms;
--duration-base: 250ms;
--duration-slow: 350ms;
/* Exit = entrance × 0.8 */
--duration-exit-fast: 120ms;
--duration-exit-base: 200ms;
--duration-exit-slow: 280ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.87, 0, 0.13, 1);
}
/* Entrance */
.dropdown[data-state="open"] {
animation: slideDown var(--duration-base) var(--ease-out) forwards;
}
/* Exit — shorter */
.dropdown[data-state="closed"] {
animation: slideUp var(--duration-exit-base) var(--ease-in-out) forwards;
}
A real example: the subscription funnel
At Vocento — Spain's largest media group, 15 brands — I redesigned the subscription paywall experience.
The first version had entrance animations on every paywall element: the overlay, the modal, the pricing cards, the CTA. Each had its own transition. The total entrance sequence was ~600ms.
Users who hit the paywall multiple times (which is the case for logged-out users on news sites) experienced that 600ms as resistance, not polish. Every visit felt like the product was making them wait before asking for money.
The fix was removing animation from the paywall entirely. Not reducing it — removing it.
The paywall appears instantly. The user sees the offer immediately. The only remaining animation is the CTA button's hover state — 150ms, ease-out.
Conversion went up. Not because we removed the animation per se, but because we stopped creating friction at the highest-stakes moment in the user journey.
The rule that emerged from that project:
Never animate elements that stand between the user and their goal. Forms, paywalls, checkout flows, error states — these exist to be acted on, not admired.
What deserves animation
The inverse: interactions rare enough that motion earns its place.
The first time a user completes a multi-step flow. A toast confirming a destructive action. An onboarding moment that only happens once.
These occur once, maybe twice per session. The motion can be intentional without accumulating cost.
The practical checklist
Before shipping any animation:
- How many times per day does a user trigger this? More than 20 times → remove or reduce to opacity only
- Is it keyboard-initiated? Yes → no animation, period
- Does this stand between the user and their goal? Yes (form, paywall, checkout) → no animation
- Does the duration stay under 300ms? No → reduce
- Does
prefers-reduced-motionfallback exist? No → add it before shipping - Does the animation communicate something? No → remove it
/* Always. No exceptions. */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
prefers-reduced-motion fallback — ship it before you ship the animation.The absence of motion is a design decision as valid as its presence. Knowing when to stop is the harder skill.