# Animation

## What it is

On the web, an animation is most often a CSS animation: a named run of
keyframes, written once in an `@keyframes` rule and attached to an element
through the `animation` properties. The W3C's CSS Animations Level 1
specification describes the mechanism as a way “to animate the values of CSS
properties over time, using keyframes”. Animated images, video and canvas
drawing move too; this article follows the declarative kind.

Each keyframe states property values at a point in the run: `from` or `0%` at
the start, `to` or `100%` at the end, and any percentages in between. The
browser fills in the values between them. The other animation properties set how
long one run lasts, how many times it repeats, whether alternate runs play in
reverse and whether it is running or paused. A repeat count of `infinite` means,
in the specification's words, that the animation “will repeat forever”.
`animation-fill-mode` decides what shows outside the run: with `none` the
animation has no effect then, `backwards` applies the first keyframe during any
delay, and `forwards` keeps the values it ended on.

A transition is the near relation: where a changed property value would normally
show at once, CSS Transitions spreads the change over a set duration. CSS
Animations draws the line between the two: transitions trigger implicitly when a
property value changes, while animations “are explicitly executed when the
animation properties are applied”. A link that fades its colour on hover is a
transition; a loading spinner that turns whether or not anything changed is an
animation.

A newer specification, Scroll-driven Animations, swaps the clock for the page
itself. An animation attached to a scroll progress timeline advances as a
scroll container scrolls, from 0% at the start of the scrollable range to 100%
at its end. When those two positions coincide, because there is nothing to
scroll, the specification calls the timeline inactive.

## Why it matters

Movement pulls attention, which is both the reason to use it and the trouble
with it.

**For some readers it is physically unpleasant.** The Understanding document for
WCAG 2.2 success criterion 2.3.3 explains that elements moving as a page scrolls
can trigger reactions in people with vestibular, or inner-ear, disorders:
dizziness, nausea and headaches. It names parallax scrolling, where backgrounds
move at a different rate from the foreground, as an animation that is often
non-essential, and notes that the impact can be severe enough to need bed rest
to recover.

**Readers can say so, and a page can ask.** Media Queries Level 5 defines
`prefers-reduced-motion`, a media feature with two values. `reduce` means the
reader has told the system they prefer an interface that “removes or replaces
the types of motion-based animation” that cause discomfort for people with
vestibular motion sensitivity or distraction for people with attention
deficits. `no-preference` means no preference is known, and it evaluates as
false when the feature is tested on its own.

**Two success criteria cover it.** 2.2.2 Pause, Stop, Hide, at level A, applies
to moving, blinking or scrolling content that starts automatically, lasts more
than five seconds and is shown alongside other content: the reader must be able
to pause it, stop it or hide it, except where the movement is essential. It
applies to everything on the page, and an animation that stops only while focus
rests on it does not count as a pause. 2.3.3 Animation from Interactions, at
level AAA, asks that motion animation triggered by an interaction can be
disabled, unless it is essential. Its Understanding document lists the
reduce-motion feature of the browser or operating system as one way to meet it.

**Not every change is motion.** WCAG defines motion animation as the “addition
of steps between conditions to create the illusion of movement or to give a
sense of a smooth transition”. An element that moves into position or changes
its size as it appears counts as animated; one that appears instantly is not. A
change of colour or opacity that leaves the element's perceived size, shape and
position alone falls outside the definition, which is why reducing motion does
not mean freezing every fade.

## What it looks like in practice

The plainest lesson starts with a rule that looked finished. The site honoured
the reduce-motion preference with a single universal rule switching every
transition off; it read as complete and passed its linter, yet under an emulated
preference eleven selectors were still animating. The work that
[made the reduced-motion setting honest](/portfolio/made-reduced-motion-honest-135/)
traced the cause to specificity: a universal selector loses to any rule that
carries a class, which is every considered animation on the page. Everything
that moves now lives in one late stylesheet part, and a linter fails a
transitioned transform anywhere else. The line it draws is reduce motion, not
colour, so a colour fade passes and a transition on movement does not. Motion is
also expressed as a transition rather than a keyframe animation, because the
first is trivially cancellable and the second is not. As the page puts it, a
rule that loses on specificity fails silently.

Scroll-driven motion has a failure of its own. Among the
[11 accessibility defects the browser reported as healthy](/portfolio/fixed-11-accessibility-defects-the-browser-called-healthy-131/)
was a scroll-driven animation, inactive on a page too short to scroll, that had
permanently clipped the footer off three pages. The automated rules had passed
it, because the defect lay in how the page rendered, not in its markup.

A linter can make the check impossible to skip. A
[48-token design system](/portfolio/built-a-48-token-design-system-160/), built
on the platform's own glass material, keeps motion as one of its six token
groups, and its linter rejects an animation declared without honouring the
reduce-motion preference. The page gives the reason: an animation is added in a
moment of polish, the preference check is the part that gets skipped, and so
the rule makes the animation impossible to write without it.

This site's stylesheets show the same habits elsewhere. The two gears in the
masthead turn on 60-second and 96-second loops, and both are set to `animation:
none` under `reduce`. Their scroll-linked parallax is gated twice, inside
`prefers-reduced-motion: no-preference` and inside an `@supports` test for
`animation-timeline: scroll()`; in a browser without the feature the gears stay
where they are, and the comment calls that fallback the design. In the footer,
the credit block that holds the copyright takes its hidden state from the
animation's backwards fill rather than from a declared `opacity: 0`. The comment
gives the reason: an inactive timeline applies no effect at all, and the old
`opacity: 0` had hidden the copyright on every short page.

## Where it goes wrong

- **The off switch loses the cascade.** A blanket `transition: none` on the
  universal selector has almost no specificity, so any rule with a class keeps
  moving and nothing reports it. A block placed after the rules it has to
  beat, naming the moving selectors, makes the still state win.
- **The preference is added later, or never.** An animation in the base style
  moves for everyone until somebody remembers the cancel. Declaring it inside
  `@media (prefers-reduced-motion: no-preference)` makes stillness the default.
- **A scroll timeline meets a page with nothing to scroll.** The timeline is
  inactive and the animation does nothing, so a starting state written into the
  base style, such as an `opacity: 0` waiting to be animated away, stays in
  force. A starting state supplied by the backwards fill goes with the
  animation.
- **A keyframe value the browser rejects drops out quietly.** The specification
  says that a property specified but invalid in a keyframe is animated as if
  that keyframe did not exist. A misspelt value skips a step without an error.
- **A decorative loop runs with no way to stop it.** An `infinite` animation
  that starts on load beside the text falls under 2.2.2 once it passes five
  seconds; a pause control, or an end within five seconds, answers it.
- **“Reduce” is read as “remove everything”.** Cutting every state change can
  take information away with the movement. Showing the new state instantly, or
  with a fade, keeps the information and drops the motion.

## Further reading

- [CSS Animations Level 1](https://www.w3.org/TR/css-animations-1/)
  — the `@keyframes` rule, the animation properties, fill modes and the events
  an animation fires.
- [CSS Transitions Level 1](https://www.w3.org/TR/css-transitions-1/)
  — how a change of property value is spread over a duration instead of being
  applied at once.
- [Scroll-driven Animations](https://www.w3.org/TR/scroll-animations-1/)
  — scroll progress timelines, the `scroll()` notation and the cases in which a
  timeline is inactive.
- [Media Queries Level 5: prefers-reduced-motion](https://www.w3.org/TR/mediaqueries-5/#prefers-reduced-motion)
  — the two values and what a reader who chooses `reduce` is asking for.
- [Understanding SC 2.2.2: Pause, Stop, Hide](https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html)
  — the five-second limit, what counts as starting automatically and which
  pause mechanisms qualify.
- [Understanding SC 2.3.3: Animation from Interactions](https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html)
  — vestibular reactions, parallax, and the reduce-motion preference as one way
  to meet the criterion.

<https://engineer.company/glossary/animation/>
