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.

Where the two limits sit on one duration scaleFlash rate is a hard safety limit; duration is a comfort limit. They constrain different ends of the same animation.Where the two limits sit on one duration scalecrispcomfortableprolongs vectionblocks the task02004006008001000 msfocus feedback 150 msopacity fade 220 msthree flashes/second floor 333 ms
Flash rate is a hard safety limit; duration is a comfort limit. They constrain different ends of the same animation.

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.

Ceilings you can encode as design tokensEach ceiling has a fallback that keeps the communicative intent when a design needs to exceed it.Ceilings you can encode as design tokensSafe ceilingFallback if exceededFlash rate3 per secondSlow the cycle or make it single-shotUI transitionduration200-500 msShorten or split into discrete stepsTranslationdistanceUnder 10% of viewportReduce distance, or swap for opacityScale deltaWithin 4%Use opacity emphasis insteadRotationNo continuous loopsOpacity pulse in placeAutoplayNone if non-essentialRequire a trigger and honour reduce
Each ceiling has a fallback that keeps the communicative intent when a design needs to exceed it.
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.

Turning the ceilings into enforceable tokensTokens make the limit reviewable: a magic number in a component is invisible to code review.Turning the ceilings into enforceable tokens1Declare one custom property per ceiling on the root, with the safe value.Reviewers see the limits in one place2Express component motion as calc() against those tokens, never as literals.No component can quietly exceed a ceiling3Override the tokens once inside the reduced-motion query.Every component follows without edits4Add a lint rule or review check for raw duration literals in component CSS.
Tokens make the limit reviewable: a magic number in a component is invisible to code review.
: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.