Skip to main content
All Docs
FeaturesMaking Tax DigitalUpdated March 26, 2026

Accessibility: Motion & Animation Safety

Accessibility: Motion & Animation Safety

As of v1.0.472, the MTD platform fully respects the OS "Reduce Motion" accessibility preference. This implements WCAG 2.3.3 (AAA) and 2.3.1 compliance across the entire application.


Who this affects

Users who have enabled the reduced-motion preference on their operating system:

OSSetting location
macOS / iOSSystem Settings → Accessibility → Motion → Reduce Motion
AndroidDisplay → Remove animations
WindowsEase of Access → Display → Show animations in Windows

When this preference is active, the browser exposes the prefers-reduced-motion: reduce media query, which the platform reads to suppress animations.


What is suppressed

When reduced motion is active, all animations and transitions are collapsed to 0.01ms (imperceptibly fast, effectively instant):

AnimationComponent(s)Behaviour with reduced motion
animate-ping (overdue dot)Sidebar quarterly deadline badge, MTD compliance ribbonRenders as a static coloured dot — no pulsing
animate-pulseLoading skeletonsNo pulsing; skeleton appears as a static shape
animate-spinLoading spinnersSpinner appears static
animate-in, fade-in-0, zoom-in-95Modals, dropdowns, notification bellElements appear instantly without entrance motion

Visual information (colour, shape, text labels) is always preserved — only the motion is removed.


Implementation details

Global CSS safety net (globals.css)

A @media (prefers-reduced-motion: reduce) block targets every element on the page:

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

Why 0.01ms instead of 0?
Setting duration to exactly 0 suppresses animationend events in some browsers, which can break JavaScript listeners that rely on them (for example, tw-animate-css state transitions). 0.01ms is imperceptible to humans but still fires events correctly.

Why animation-iteration-count: 1?
Infinite-loop animations (animate-ping, animate-spin) would otherwise run exactly one imperceptibly-fast loop and then freeze in a mid-animation CSS state. Setting iteration count to 1 ensures they complete and rest in their natural end state.

Component-level gating (motion-safe: Tailwind variant)

The two overdue-quarter pulsing dots — which loop continuously whenever a submission deadline is missed — are additionally gated at the component level with Tailwind's motion-safe: variant:

// sidebar-nav.tsx — QuarterlyDeadlineBadge
<span className="absolute inline-flex h-full w-full motion-safe:animate-ping rounded-full bg-red-500 opacity-75" />

// mtd-compliance-ribbon.tsx — MtdComplianceRibbon
<span className={[
  "absolute inline-flex h-full w-full motion-safe:animate-ping rounded-full opacity-75",
  colours.dot,
].join(" ")} />

With motion-safe:animate-ping, the class is simply not applied when the user has reduced motion enabled — the element renders as a static dot with no animation CSS attached at all.

Defence-in-depth

Both layers are applied together intentionally:

┌─────────────────────────────────────────────────────────┐
│  Global CSS rule (globals.css)                          │
│  Collapses ALL animation/transition utilities app-wide  │
│  ↳ Safety net for any component not explicitly gated    │
├─────────────────────────────────────────────────────────┤
│  motion-safe:animate-ping (sidebar-nav, ribbon)         │
│  Explicit opt-in on the two highest-impact animations   │
│  ↳ Perpetually-looping overdue dots get precise control │
└─────────────────────────────────────────────────────────┘

WCAG compliance

CriterionLevelDescription
2.3.3 Animation from InteractionsAAAMotion triggered by interaction can be disabled
2.3.1 Three Flashes or Below ThresholdAContent does not flash more than three times per second

Adding new animated components

When adding animations to any new component, follow this pattern:

  1. Perpetually looping animations (animate-ping, animate-spin used as indefinite spinners): use the motion-safe: Tailwind variant explicitly.

    // ✅ Correct
    <span className="motion-safe:animate-ping" />
    
    // ❌ Avoid for infinite loops
    <span className="animate-ping" />
    
  2. Entrance / one-shot animations (animate-in, fade-in-0, etc.): the global CSS safety net in globals.css covers these automatically, but adding motion-safe: is also acceptable and encouraged for clarity.

  3. Do not use animation-duration: 0 to suppress motion — use 0.01ms to preserve animationend event firing.