Skip to content

Perceived Speed

A UI principle for coding agents. Also covers perceived performance, feels slow, feels fast, snappy, responsiveness, easing choice, and 1 more.

Show all 7 aliases

perceived performance, feels slow, feels fast, snappy, responsiveness, easing choice, sluggish ui

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-in and again with ease-out
  • Both take exactly 300ms; the ease-out one feels noticeably quicker
  • Ship ease-in everywhere 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-visible style, 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 linear when the motion is the measurement: hold-to-confirm, countdowns, progress fills
  • Any eased curve here misreports how much time is left
  • Use linear for 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-in appears only on elements leaving the screen
  • Pointer press feedback stays around 0.97 and within 150ms
  • Keyboard users get a visible :focus-visible style, 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

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-ui-principle({ topic: "perceived-speed" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems