---
title: "Using motion"
description: "Motion chosen by the job it does, with duration derived from distance. The five jobs are enter, exit, feedback, navigation and loading, plus the count-up rule."
url: "https://opsinjs.pensievelabs.org/foundations/motion/using-motion"
source: "https://opsinjs.pensievelabs.org/foundations/motion/using-motion.md"
section: "Foundations"
kind: "foundation"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["transition", "enter animation", "exit animation", "count-up"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="foundation" />

## Overview [#overview]

The usual way to choose an animation is to pick a duration that feels right and a
curve that looks nice. That produces a system where every component is
individually plausible and the whole is incoherent: a sheet that takes 400 ms
next to a menu that takes 150 ms, for no reason either author could state.

opsinjs chooses by **job**. There are five, they behave differently, and each one
has a defensible reason for its curve and its duration. Once you have named the
job, the token follows, and the argument is over.

The related idea people reach for instead is a t-shirt scale of durations named
`fast`, `medium` and `slow`. opsinjs has those steps, but they are the output
of the decision, not the input. Choosing `slow` because a transition felt
rushed is how a system ends up with a 560 ms press feedback.

## How it works [#how-it-works]

**Enter.** Something arrives that was not there. Decelerating: fast at the start,
settling at the end, so the eye catches the movement and then the destination.
Enter transitions may overshoot when the arriving thing is chrome, and must not
when it contains a value. Enter is also the only job allowed to be the longest,
because the reader is being told where something came from and that takes a
moment to read.

**Exit.** Something leaves. Accelerating and *shorter than its enter*. Its
duration is usually around two thirds of the enter's. Nobody needs to watch
something they have dismissed; a symmetrical exit reads as the interface
arguing with you. Exit is also where `data-ending-style` matters, because the
element must survive long enough to animate before it is removed from the DOM.
The mechanics are in
[Handbook → Motion in practice](../../handbook/motion-in-practice.mdx).

**Feedback.** The interface acknowledging your touch. The shortest thing in the
system, on the `instant` step, applied on pointer-down rather than on click.
Anything longer than a blink stops reading as acknowledgement and starts reading
as lag. On a form where somebody is entering a blood-pressure reading, that is
the difference between one entry and two.

**Navigation.** Moving between screens or between levels of one. This is where
[choreography](./choreography.mdx) lives, and where the depth-on-overlay rule
applies: a surface arriving over another pushes the one underneath back rather
than sliding it aside.

**Loading.** The only job that legitimately loops, and therefore the only one
that legitimately uses keyframes. A shimmer or a progress indicator has no end
state to transition to. Everything else has two states and belongs in a
transition.

**Duration is a function of distance, not of importance.** A thing that moves
across the screen takes longer than a thing that moves four pixels, because at a
constant duration the long move looks slow and the short move looks like a
glitch. In practice this means the scale is used as: `instant` for feedback,
`fast` for small local changes and exits, `base` for most enters and in-place
changes, `slow` for a full-width or full-height surface, and `deliberate` for
the count-up described below and almost nothing else.

## Using it [#using-it]

**Name the job before you pick a token.** If you cannot say which of the five it
is, the transition probably should not exist.

**The count-up rule.** Animating a number from zero to its value is permitted
exactly once per value, on first reveal, and for no longer than 600 ms.

* *Once* means on the first render of that value on that screen. A pull to
  refresh that returns the same number does not re-animate it; a return from a
  background tab does not re-animate it. An unchanged number that animates is
  telling the reader something changed.
* *600 ms* is a ceiling, not a target. It is opinion rather than a measured
  threshold, and the reasoning is simple: the count-up exists to draw the eye to
  the figure the screen is about, and past about half a second the reader who
  already found it is being made to wait for a decoration.
* The number is present and correct in the DOM from the first frame. What
  animates is the *presentation*; assistive technology is given the final value
  immediately and never the intermediate ones.

<DoDont>
  <DoDont.Do>
    A daily step count that counts up once when the screen first appears,
    finishing well inside 600 ms, with the final figure in the accessible name
    from the start.
  </DoDont.Do>

  <DoDont.Dont>
    A resting heart rate that counts up every time the tab regains focus. Nothing
    has changed, the animation says something has, and a reader watching their
    own heart rate is the last person who should be given a false signal of
    change.
  </DoDont.Dont>
</DoDont>

**Transitions, not keyframes, wherever a reader can interrupt.** Anywhere
somebody can scroll, tap, dismiss or navigate mid-animation, use a CSS
transition between two states. That is nearly everywhere. A transition reverses
smoothly from wherever it has got to; a keyframe animation either restarts from
the beginning, snaps, or holds the reader until it finishes. The single exception
is the looping loading indicator, which has no end state.

<DoDont>
  <DoDont.Do>
    A sheet that transitions between closed and open. Start to drag it back
    halfway through the opening and it follows your finger from where it is.
  </DoDont.Do>

  <DoDont.Dont>
    The same sheet as a keyframe animation. Interrupting it makes it jump to the
    start or lurch to the end. A bottom sheet is the surface most likely to be
    grabbed mid-flight, and that is where the failure is most noticeable in the
    system.
  </DoDont.Dont>
</DoDont>

**Never put information only in the motion.** If the only way to know two things
are related is to have watched them animate, the relationship is invisible to
anybody who arrived after it finished, took a screenshot, or has reduced motion
on.

<MotionDemo easing="--opsin-ease-spring-settle" duration="--opsin-duration-spring-settle" property="translate" label="A popover settling is the chrome workhorse" />

## Tokens [#tokens]

Durations and easings are generated from `tokens/motion.json` by
`scripts/build-tokens.mts`. Which job maps to which step is documented in the
`What it controls` column of the generated table rather than restated here.

<TokenTable scope="motion" />

## Accessibility impact [#accessibility-impact]

* **Feedback timing is a motor-accessibility issue.** A press acknowledgement
  that arrives late reads as a missed tap, and a reader with a tremor or reduced
  dexterity will tap again. On a logging surface, that means a duplicate entry.
  This is why feedback is on the shortest step and fires on pointer-down.
* **Interruptibility is an accessibility requirement, not a polish item.**
  Somebody using a switch or a screen reader may act at any point in an
  animation. A transition handles that; a keyframe sequence often does not.
* **An animated number must not be an animated announcement.** The count-up is
  visual only. Screen readers receive the final value once. A live region that
  fires on every intermediate frame is unusable, and it is a common consequence
  of animating the text content instead of the presentation.
* **Nothing may be unusable while it animates.** Controls accept input during
  their entrance; a reader who knows where the button is does not wait.
* **Every job has a reduced variant**, and none of them are simply removed. See
  [Reduced motion](./reduced-motion.mdx).

## Related [#related]

* [Choreography](./choreography.mdx) says what happens when more than one thing
  has a job at the same time.
* [Springs as tokens](./springs-as-tokens.mdx) is where the curves these jobs use
  come from, and which of them may touch a value.
* [Data states](../data-states.mdx) is the loading job's other half: what the
  skeleton is allowed to look like.
