Choosing Safe Animation Durations and Thresholds

Part of Vestibular-Safe Motion Patterns in Accessible Motion Architecture.

The problem: “keep it subtle” is not a specification

Design reviews reject motion that is obviously too much, but “subtle” is not a number an engineer can implement or a checker can verify. Two distinct kinds of limit are hiding under that word. One is a hard safety limit — the WCAG three-flash rule — where crossing the line can trigger a photosensitive seizure. The other is a set of perceptual thresholds for vection and comfort, where crossing the line makes vestibular users unwell without any bright-line spec. This page turns both into concrete ceilings you can put in code.

Root cause: two different failure modes, two different limits

The flash limit and the vection limits fail for unrelated physiological reasons, so they need to be reasoned about separately.

Flashing is a temporal phenomenon. Rapid, large changes in luminance — more than three per second over a sufficiently large area — can provoke seizures in people with photosensitive epilepsy. This is the concern behind WCAG 2.3.1 Three Flashes, and it applies to any pulsing opacity, alternating background-color, or flickering animation regardless of displacement. It is a hard limit: there is a specific rate you must not exceed.

Vection is a spatial phenomenon. Large, coherent displacement across the visual field reads as self-motion, as explained in the vestibular-safe motion overview. Its severity scales with the area that moves, the distance it travels, and its velocity — a small dot sliding a few pixels is nothing; a full-viewport plane sweeping across the screen is a strong trigger. There is no single spec number, so you work from heuristics and gate anything ambiguous behind prefers-reduced-motion.

Because the two are independent, an animation can pass one and fail the other. A slow full-screen slide never flashes but is a serious vection hazard; a tiny icon flickering ten times a second never moves far but breaches the flash limit. You must check both axes.

Decision table: thresholds by motion type

Use this as the concrete specification. Values in the “safe ceiling” column are the limits to stay under; anything above them belongs behind a reduced-motion gate or should be removed.

Dimension Safe ceiling Why Fallback if exceeded
Flash rate ≤ 3 per second (aim well under) WCAG 2.3.1; seizure risk Slow the cycle; make it single-shot
UI transition duration ~200–500 ms Longer feels sluggish and prolongs any vection Shorten; split into discrete steps
Translation distance < ~10% of viewport dimension Larger areas of flow read as self-motion Reduce distance; swap for opacity
Scale delta within ~±4% (scale(0.96)scale(1.04)) Large zoom reads as approach Reduce delta; use opacity emphasis
On-screen velocity Low; avoid fast sweeps Speed intensifies optical flow Ease-out to bleed off velocity
Rotation No continuous loops Rotational vection Replace with opacity pulse
Autoplay None for non-essential motion Removes user control Require a trigger; honour reduced-motion
Opacity fade (reduced-motion) ~150–250 ms, single-shot No displacement; brief and non-repeating Acceptable as the safe default

The last row is the important reassurance: an opacity fade of around 200ms is not just tolerated under reduced motion, it is the recommended replacement for suppressed movement. It contributes no vection and, kept single-shot, never approaches the flash limit. “Reduced motion” should mean this, not a dead interface.

Production code: thresholds encoded as tokens

Encoding the ceilings as custom properties makes them auditable and keeps every animation on the site honest. The full-motion branch stays within the distance and duration caps; the reduced-motion branch drops to a brief, non-directional fade.

:root {
  /* Safety and perceptual ceilings, expressed once and reused everywhere */
  --motion-duration-ui: 300ms;   /* within the 200–500 ms UI cap */
  --motion-distance-max: 8px;    /* well under 10% of a typical viewport */
  --motion-fade: 200ms;          /* reduced-motion fallback fade */
}

/* Full-motion: small rise within the distance cap, quick enough to feel crisp */
@media (prefers-reduced-motion: no-preference) {
  .toast-enter {
    animation: toast-in var(--motion-duration-ui)
               cubic-bezier(0.22, 1, 0.36, 1) both;
  }
  @keyframes toast-in {
    from { opacity: 0; transform: translateY(var(--motion-distance-max)); }
    to   { opacity: 1; transform: translateY(0); }
  }
}

/* Reduced-motion: opacity only, brief and single-shot — no displacement, no flashing */
@media (prefers-reduced-motion: reduce) {
  .toast-enter {
    animation: toast-fade var(--motion-fade) ease both;
  }
  @keyframes toast-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
}

Rendering Impact: opacity + a clamped transform — composite only; the reduced-motion branch removes the transform, leaving a brief non-directional fade that satisfies both the flash and vection limits.

Because the distance and duration are tokens rather than magic numbers, a review can grep for a raw translateY(60px) or a 2s UI transition and flag it against the ceiling, which is far more reliable than eyeballing “subtlety”. The specific reduced-motion cascade patterns that consume these tokens are covered in prefers-reduced-motion architecture.

Verification checklist

Constraints and trade-offs

  • The flash rate is a hard safety limit, not a comfort preference; it applies to every user, so it cannot be gated behind prefers-reduced-motion — it must always hold.
  • The distance and velocity ceilings are heuristics, not spec numbers; when a motion sits near a limit, prefer the safer choice because the query cannot tell you how sensitive an individual user is.
  • Very short durations can themselves cause an abrupt, jarring change; the 200–500 ms window balances “quick” against “sudden”, and dropping to near-zero is only appropriate when replacing motion with an instant state change.
  • Tokenising thresholds only helps if the tokens are actually reused; a hard-coded distance elsewhere in the codebase bypasses the whole safety net, so enforce it in review.
  • A brief opacity fade is safe, but a rapidly looping opacity pulse is not — repetition can breach the flash rule even though a single fade is fine.