Skip to content

Motion & Easing

Overshoot

Motion that travels slightly past its target before settling back, giving a small sense of energy.

Overshoot carries an element a little past its target before it settles back, so the arrival has some energy, like a ball that rolls a touch too far. It comes from a back curve, a cubic-bezier whose first y value is above 1, or from a spring with bounce: bounce is the setting, overshoot is what you see, and a bounce of 0 never overshoots. The amount is a share of the distance travelled, so the same curve reads stronger on a long move than a short one. Around 2 to 5% reads as life; more reads as a cartoon.

Also called bounce-in, back easing, goes a bit past and comes back, elastic, pop in, easeOutBack

Use when

  • Playful, physical arrivals: a like, a toggle knob, a card released into its slot after a drag.
  • Success states and rare first-time moments, like finishing onboarding.

Avoid when

  • Dialogs, menus, popovers and icon swaps. A wobble on something that informs or interrupts reads as unsteady; use ease-out.
  • Anything used often or meant to feel precise, like a slider thumb, a tab indicator or a drag that follows the finger.
  • Exits and returns. Overshoot marks an arrival; leaving stays plain and faster.
  • Text and opacity. Text that wobbles is hard to read, and opacity clamps at 1, so an overshooting fade just ends early.
  • More than about 5% in product UI, or any spring bounce over 0.3.
  • Changes from the keyboard. Do not animate them.

Prompt

Give this element a slight overshoot when it arrives.

- Setup: a `<button type="button">`; each click toggles it between its start (`translateX(0)`) and its target (`translateX(240px)`, the travel) and sets `aria-pressed`.
- Arrival: `transform` only, 300ms, `cubic-bezier(0.34, 1.3, 0.64, 1)`: 3% of the travel past the target; settled on the target by 300ms from the first animated frame.
- Return: 225ms `cubic-bezier(0.23, 1, 0.32, 1)` (ease-out), no overshoot.
- CSS route: `[data-placed]` (at the target) holds the arrival (300ms, the curve above); the base rule holds the return. Motion route: `animate={{ x }}`, `initial={false}`, `{ duration: 0.3, ease: [0.34, 1.3, 0.64, 1] }` on the way in.
- Only `transform` (translate or scale) takes the curve: never `opacity` (it clamps at 1) or layout properties. Not on exits, dialogs, menus or text.
- Keyboard click (`event.detail === 0`) or reduced motion: no animated travel; at the target next frame. CSS: set `data-instant` from `event.detail === 0`, `[data-instant] { transition-property: none }` after the placed rule, plus `@media (prefers-reduced-motion: reduce)`; Motion: `useReducedMotion()`, `duration: 0` chosen in the same render as `x`.

Acceptance checklist

  • Sampled every frame on the way to the target, `x` peaks once at 240 × 1.030 = 247.2px (±1% of the travel, ±2.4px), then settles.
  • By 300ms from the first animated frame, `x` is within 0.5px of 240px and stays there.
  • Returning, `x` never goes below -0.5px and is home by 225ms: no overshoot on the way back.
  • Only `transform` changes: `opacity` stays put and `offsetLeft` never changes.
  • Activated with Enter or Space, or with reduced motion on, the element is painted at its target in the first frame after the event, with no in-between positions.

Code

:root {
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  /* y1 above 1 is the overshoot: about 3% of the travel past the target.
     The common easeOutBack, (0.34, 1.56, 0.64, 1), goes about 10% past. */
  --arrive: cubic-bezier(0.34, 1.3, 0.64, 1);
}

/* Markup: <button class="chip" aria-pressed="false">…</button>; a click
   toggles data-placed and aria-pressed. Transform only: a curve past 1 pushes
   any property past its end (opacity clamps at 1, layout relayouts every frame). */
.chip {
  transition-property: transform;
  /* The base rule times the return (a transition takes the timing of the state
     it heads to): plain and faster, since overshoot marks an arrival. */
  transition-duration: 225ms;
  transition-timing-function: var(--ease-out);
}
.chip[data-placed] {
  transform: translateX(240px);
  transition-duration: 300ms;
  transition-timing-function: var(--arrive);
}

/* Keyboard clicks land at once: set data-instant when event.detail === 0 (Enter or Space). */
.chip[data-instant] {
  transition-property: none;
}

/* Reduced motion: in place at once, no travel and no overshoot. */
@media (prefers-reduced-motion: reduce) {
  .chip {
    transition-property: none;
  }
}

Accessibility

Overshoot is extra movement, so under reduced motion drop it along with the travel: put the element at its target at once, or for something entering, fade it in over 150ms. Changes made from the keyboard land at once too. Update the state (`aria-pressed`, checked) on the click, not when the motion settles.

  • Spring

    Motion driven by physics instead of a fixed duration, so it can carry velocity and settle naturally.

  • Ease-out

    Motion that starts fast and slows into place, the default for anything responding to the user.

  • Press Scale

    A button shrinks slightly while pressed, so the click feels physical.

  • Icon Swap

    One icon turns into another in place with a quick scale, fade and blur, like copy becoming a check.

Often confused with

  • Overshoot vs SpringA spring is the physics; overshoot is one thing a spring (or a back curve) can do. A spring with bounce 0 never overshoots.