How fast an interface feels matters more than how fast it measures. Two animations of identical duration can feel seconds apart depending on where the motion is front-loaded. Reach for these levers before optimising real load time.
Easing Changes Perceived Duration
Same duration, different feel. An ease-in animation starts slow, so the user waits before seeing movement and reads the whole interaction as sluggish. An ease-out animation starts fast and settles, so it reads as immediate.
- Run a dropdown at 300ms with
ease-inand again withease-out - Both take exactly 300ms; the
ease-outone feels noticeably quicker - Ship
ease-ineverywhere and the whole app feels slower than it is
Default to ease-out for anything the user triggered. Opening a menu, a modal, a popover. The fast start is the acknowledgement that their input landed.
.dropdown {
transition: opacity 300ms cubic-bezier(0.165, 0.84, 0.44, 1);
}
Avoid Ease-In For Interface Motion
Ease-in accelerates towards the end, which is backwards for interface work. Movement in the physical world settles as it arrives, so an animation that speeds up at the finish reads as unnatural as well as slow.
Shortening the duration does not rescue it. The shape of the curve is the problem, not its length.
Exception: elements leaving the screen entirely. Nothing needs to settle if it is gone.
Acknowledge Input Instantly
The cheapest perceived-speed win is confirming the press before any work starts. A small scale-down on :active suits pointer and touch input, where the control is being physically pushed.
.button:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
.button {
transition: transform 150ms cubic-bezier(0.165, 0.84, 0.44, 1);
}
.button:active {
transform: scale(0.97);
}
}
Declaring the press inside the gate rather than switching it off inside a reduce block means a browser that cannot evaluate the query gets no motion, instead of getting all of it. accessibility.md has the argument. The focus ring stays outside the gate, because it is not motion and must never depend on the preference.
Around 0.97 reads as a press; below about 0.95 it reads as a bounce.
Three things this scale cannot do on its own:
- Keyboard users never trigger it. They need a visible
:focus-visiblestyle, which is a separate requirement rather than a nicety - Reduced-motion users should not receive it. Drop the transition, keep the control usable
- It is feedback, not confirmation. A press that starts a request still needs a loading or success state
Match Motion To Real Progress
Perceived speed is not always about seeming faster. When an animation represents time or progress passing, it must track reality or it becomes a lie the user can feel.
- Use
linearwhen the motion is the measurement: hold-to-confirm, countdowns, progress fills - Any eased curve here misreports how much time is left
- Use
linearfor continuous ambient motion too, such as a marquee
Everywhere else, avoid linear. Constant velocity reads as robotic because nothing in the physical world starts and stops instantly.
Checklist
- User-initiated transitions use ease-out, not ease-in
-
ease-inappears only on elements leaving the screen - Pointer press feedback stays around 0.97 and within 150ms
- Keyboard users get a visible
:focus-visiblestyle, not only the press scale - Motion is dropped under
prefers-reduced-motion - Progress and hold interactions use
linear - Marquees and ambient loops use
linear - Duration was not shortened to compensate for a badly chosen curve