Reduced motion, not zero

Keep fades under reduced motion, drop movement, gate hover.

Emil Kowalski@emilkowalski
Great animations · emilkowal.skiRead article ↗

From emil-design-eng and his animations.dev lessons.

Kill it all, or keep the fade

Open the dialog, press Archive, then switch modes.

animation: noneBad
  • Q3 report2d
  • Invoices, Sep5d
  • Payroll1w
transition: none
Keep the fadeGood
  • Q3 report2d
  • Invoices, Sep5d
  • Payroll1w
transition: opacity 200ms ease
.toast {
  transition: transform 250ms var(--ease-out), opacity 200ms ease;
}
.toast[data-open="false"] { opacity: 0; transform: translateY(100%); }

@media (prefers-reduced-motion: reduce) {
  .toast { transition: opacity 200ms ease; }
  .toast[data-open="false"] { transform: none; }
}

// JS motion never sees the media query.
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;

Tap a card on touch

Tap a card in each pane, then tap another.

Ungated :hoverBad
.card:hoverstuck: –
Gated hoverGood
@media (hover: hover)stuck: –
.card { transition: transform 200ms var(--ease-out), box-shadow 200ms ease; }
.card:active { transform: scale(0.97); }

@media (hover: hover) and (pointer: fine) {
  .card:hover { transform: translateY(-3px); box-shadow: var(--shadow); }
}

Why it matters, pros and cons

Why it matters

Movement causes nausea; a fade does not. Ungated hover sticks after a tap.

Pros

  • Everyone still sees state changes.
  • One media query, no second code path.
  • No stuck hover on phones.

Cons

  • JS springs ignore CSS; check matchMedia too.
  • Keep motion that is content, like spinners.
  • Hover-only actions need another way in on touch.