# Motion And Transitions

> Use motion to aid comprehension in data interfaces - transitions that preserve object constancy when data changes, entry and update choreography, easing and duration choices, what should never animate, and honouring reduced-motion. Trigger on "animation", "transition", "animate the chart", "make it feel smooth", "easing", "reduced motion", "should this move", "loading animation".

- Skill: `lukehle/motion-and-transitions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lukehle/motion-and-transitions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lukehle/motion-and-transitions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Lukehle (https://skillmd.com/u/lukehle)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lukehle/motion-and-transitions

---


# Motion and transitions

Motion in a data interface has exactly one job: **help the reader keep track of what changed.**

Anything else — motion that decorates, that delays, that draws attention to itself — costs time and
credibility with an audience reading financial information.

The test for any animation: *if I removed this, would the reader understand the change less well?*
If no, remove it.

---

## The one case where motion genuinely earns its place

**Object constancy.** When data updates and marks persist, animating between states lets the eye
follow individual marks instead of re-reading the whole chart.

```
Filter changes → bars keep identity, heights transition → the eye tracks which grew
Filter changes → chart is replaced instantly       → the reader re-reads everything
```

This is real comprehension value, and it is the main reason to animate a chart at all. It requires
keying marks by identity, not by index:

```js
// Bars must be matched by what they represent, not by position, or a re-sort
// animates the wrong bar into the wrong place and actively misleads.
const key = d => d.account;
```

A re-sorted chart animated by index is worse than no animation: it shows a mark moving that did not
move.

---

## Durations

| Interaction | Duration |
|---|---|
| Hover / focus feedback | 80-120ms |
| Small state change (toggle, expand) | 150-200ms |
| Chart data transition | 250-400ms |
| Page or panel entry | 200-300ms |
| Anything longer | Almost certainly wrong |

Under ~80ms reads as instant (so the animation is wasted). Over ~400ms the reader is waiting, and a
dashboard that makes you wait to see a number is a dashboard people stop using.

**A staggered entry across many marks must have a total budget**, not a per-item delay: 30 bars ×
50ms is 1.5 seconds of nothing. Cap the total at ~300ms and shrink the per-item delay to fit.

---

## Easing

| Easing | Use |
|---|---|
| `ease-out` (`cubic-bezier(0, 0, 0.2, 1)`) | Entering, expanding, appearing — fast then settling |
| `ease-in` (`cubic-bezier(0.4, 0, 1, 1)`) | Leaving, collapsing |
| `ease-in-out` | Moving between two on-screen positions |
| `linear` | Progress indicators and continuous rotation only |

Default to `ease-out` for almost everything. `linear` on a UI transition reads mechanical; springs
and bounces read playful, which is wrong for financial data.

---

## Animate only transform and opacity

Those two are composited and do not trigger layout. `width`, `height`, `top`, `left`, and `margin`
force layout on every frame and will drop frames on a modest laptop.

```css
/* costly */ .panel { transition: height 200ms; }
/* cheap   */ .panel { transition: transform 200ms, opacity 200ms; }
```

For SVG chart marks, animating the `d` attribute or `height` is unavoidable in places — so cap the
number of simultaneously animating elements. Above a few hundred, animate the group with a single
transform, or skip the animation. See `artifact-performance`.

---

## Never animate these

- **A number counting up.** It is unreadable during the count and adds nothing at the end. This is
  the most common decorative animation in dashboards and it costs the reader time.
- **Axis scales, silently.** If the axis rescales, the marks change size without the data changing —
  a misread. Either hold the scale or make the rescale explicit.
- **Anything on load that delays the first read.** The page should paint its content, not perform.
- **Tooltips.** They must appear instantly; a fade makes the interface feel laggy.
- **Anything that loops.** A perpetual animation in a data view is a distraction with no information
  content — the one exception being an active-progress indicator.
- **Colour on a semantic scale.** Animating between two colours passes through intermediate values
  that mean something else on the scale.

---

## Loading, done properly

```
0-100ms      nothing - it will be over before it is perceived
100ms-1s     skeleton in the shape of the result
1s-5s        skeleton plus a subtle progress indication
> 5s         explicit progress, an estimate, and a cancel
```

**A skeleton must match the final layout's dimensions.** A skeleton that is a different size than the
result produces a layout jump, which is more jarring than no skeleton at all.

Prefer a skeleton to a spinner: it communicates *what is coming*, and it makes the wait feel shorter
because structure appears immediately.

---

## Reduced motion is non-negotiable

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
```

Two things people get wrong:

1. **Reduced motion means no motion, not slower motion.** The trigger is movement itself.
2. **The end state must be correct with every transition removed.** If an element is only visible
   because an animation set its final opacity, it will be invisible for these users. Test by
   disabling animations entirely and confirming the page is complete and correct.

For a JS-driven animation, check the preference directly:

```js
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
setState(next, { duration: reduce ? 0 : 300 });
```

---

## Related skills

- `artifact-accessibility` — reduced motion as a requirement
- `artifact-performance` — the frame budget
- `app-interaction-patterns` — the states motion transitions between
- `ui-antipatterns` — decorative motion as a generated-page tell

