Micro-animation: the 100ms rule, and everything else isβ¦
Motion that earns its place versus motion that is decoration. Why acknowledgement beats a toast, why the browser gives you two free properties, and theβ¦
Most writing about interface animation is about how to make things move. The harder question is which things should, and the answer is narrower than the tooling suggests.
My working rule: motion earns its place if it explains something. Where something came from, that it is loading, that a press registered, that this element is now that element. Everything else is decoration, and decoration is the first thing to feel dated.
The rule that matters more than all the rest
Every action is acknowledged at the control, within about a tenth of a second.
Not somewhere else on the page. At the control the person just pressed, in the moment they pressed it. A hundred milliseconds is roughly where an interface stops feeling like it responded and starts feeling like it is thinking.
The failure I see most: a copy button that copies successfully and shows nothing. It is indistinguishable from a copy button that failed, so the person presses it again, and still does not know. The fix is not a toast in the corner β it is the icon becoming a tick, right there, for two seconds.
A toast is the fallback, not the default. It answers a question in a place nobody was looking.
And the visual change is not enough on its own. A swapped icon is invisible to a screen reader, so the state change is announced separately in a live region. Those are two different mechanisms for two different people, and shipping one is shipping half.
Two properties are cheap, the rest are not
transform and opacity can be handled by the compositor without recalculating layout or repainting. Everything else β width, height, top, left, margin, padding β forces layout on every frame.
/* janky: layout on every frame */
.card:hover { margin-top: -4px; }
/* smooth: compositor only */
.card { transition: transform 180ms ease-out; }
.card:hover { transform: translateY(-4px); }
On a desktop with a fast machine you will not see the difference. Inside a WebView on a mid-range Android phone β which is where a large share of my users actually are β you absolutely will, and it reads as the app being slow rather than the animation being wrong.
Durations, and why long ones are the giveaway
What I use: 100β150ms for a hover or press, 180β250ms for something entering, up to 400ms for a full-page or route transition. Entrance animations cap at 600ms including any stagger.
Anything longer is an animation being admired rather than used. The test is the tenth time: a 600ms flourish is delightful once and is a tax every time after, and your users are on the four hundredth time, not the first.
Easing follows physics rather than taste. Things entering decelerate β ease-out. Things leaving accelerate β ease-in. Linear reads as mechanical for anything spatial, and is correct for a progress indicator, which is not spatial.
reduced-motion is not a preference to respect politely
For some people, parallax and large motion cause actual nausea and vertigo. This is a medical setting, not a taste setting.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
The blunt version above is a floor rather than a finished answer. The better version keeps the acknowledgement β a colour change, an instant state swap β and removes the movement, because the person still needs to know their press registered. Removing all feedback is a different bug wearing an accessibility badge.
Pointer-dependent life is marketing-only
Hover lifts, cursor followers, magnetic buttons. All of these are lovely on a laptop and completely absent on a phone, which is most of your traffic.
That is fine on a landing page, where the goal is impression. It is not fine as the only feedback in an application, because a touch user then gets an interface where nothing responds. Every hover effect in an app has a :active or focus counterpart, or it does not exist for half the people using it.
The related trap I have seen ship: hiding the native cursor to show a custom one, where showing and hiding are two separate pieces of code. Get one wrong and the page has no pointer at all β which is unusable, and only on someone else's machine.
Skeletons, spinners, and the layout jump
Loading states are where motion does the most work and gets the least thought.
A centred spinner tells you something is happening and nothing else. When the content lands it appears at a different size in a different place, and the page jumps. A skeleton shaped like the thing it is replacing β same grid, same card, same three text rows β does two jobs: it says what is coming, and it reserves the space so nothing moves when the data arrives.
The rule I follow is that the skeleton has the same layout as the content, not merely a similar spirit. A skeleton with three rows standing in for content with five is a layout jump with extra steps.
Above roughly a second, a skeleton stops being enough and the interface should say what it is waiting for. Below about two hundred milliseconds, showing anything at all is worse than showing nothing β a flash of skeleton that appears and vanishes reads as a glitch, and the honest fix is a short delay before the loading state appears at all, so a fast response never triggers it.
Where I use a library, and where I do not
Almost all of the above is CSS transitions and a handful of keyframes. I reach for an animation library in two situations: shared-element transitions, where something has to appear to be the same object in a new position, and gesture-driven motion that has to follow a finger and settle physically when released.
Both are genuinely hard and both are worth a dependency. A hover lift is not. Adding a runtime dependency, and the maintenance it implies for as long as the project lives, to replace four lines of CSS is a trade that only looks good on the day you make it.
What I actually ship
A short entrance for content as it arrives, with a small stagger so a list does not appear as one block. A hover lift on cards, with a focus equivalent. A visible :focus-visible ring on everything interactive, because removing outlines because they are ugly is how a keyboard user loses track of where they are. Skeletons shaped like the thing they replace, so nothing jumps when the data lands.
Almost all of that is CSS transitions. I reach for an animation library when there is shared-element movement or gesture-driven motion, and not before β a dependency for a hover lift is a dependency you maintain forever for something four lines of CSS already did.
https://aoneahsan.com/blog/micro-animations-framer-motion-tips