Shipped on day one, never as a retrofit.
This site has a scroll-scrubbed frame sequence, a pinned split-scroll, a page transition plane, a letter-by-letter chapter card and a drifting marquee. Under prefers-reduced-motion every one of them becomes a 120 millisecond cross-fade, and the identity survives.
Why it is built this way round.
-
Reduced motion is not a downgrade path
If the reduced-motion version is built afterwards, it is always a subtraction: things stop appearing, layouts collapse, and content that only existed inside an animation goes missing.
Built first, it is the base case. The motion is the enhancement, and removing an enhancement cannot break anything.
The still version is the design. The moving version is a performance of it.
-
Nothing that matters lives inside an animation
Every scrubbed frame has a first frame that stands alone. Every pinned sequence is a legible stack of blocks when it is not pinned. Every staggered reveal starts at full opacity when the stagger is off.
That is a content rule rather than a CSS rule, and it is the one that actually does the work.
-
The chapter card still stamps, it simply does not perform
The animatic transition is 1200 milliseconds with two deliberate stutter holds in it, and those holds are the whole difference between an animatic and a corporate scroll library.
Under reduced motion it is a 120 millisecond cross-fade. The number still appears, the gerund still appears, the beast still appears. Nobody is told less.
-
And the global rule is a floor, not the whole answer
The stylesheet does clamp every animation and transition to 0.01ms under the media query, which catches anything a component author forgot.
But a blanket clamp on its own produces things that snap rather than things that were designed to be still, which is why each of the five big motion pieces has its own named reduced state.
What each piece degrades to.
| Motion | Normally | Under reduced motion |
|---|---|---|
| The chapter card | 1200ms animatic with two stutter holds | 120ms cross-fade. Everything still appears |
| Scroll-scrubbed frames | A frame sequence driven by scroll position | A single frame and a caption. Which is a storyboard, which is honest |
| Pinned split-scroll | The plate holds while the copy walks past | An ordinary stack of blocks, in order |
| The page transition plane | A skewed wipe across the viewport | No wipe. The next page is simply there |
| The marquee | A slow horizontal drift | Static, wrapped, and readable |
| Staggered reveals | 40ms per item, translated up | Present at full opacity, no transform |
| Parallax layers | Offset against scroll | Not offset |
| Odometers | Counting up on entry | The final number, immediately |
How it is enforced.
The global floor
- Media query
- @media (prefers-reduced-motion: reduce)
- Animation duration
- Clamped to 0.01ms, with !important
- Iteration count
- Clamped to 1
- Transition duration
- Clamped to 0.01ms
- scroll-behavior
- auto, so anchor jumps do not scroll
The named replacements
- Chapter card
- A 120ms opacity fade, applied to all four of its parts
- Scrub sequences
- Runtime does not attach; the first frame stays
- Split-scroll
- Runtime does not pin; the steps render as a stack
- Reveal helpers
- Opacity 1 and transform none, forced
Also capped, for everybody
- Split-scroll sections
- Two per page maximumMore than that and the site becomes a showreel rather than a place to buy jam
- Marquee
- One per page
- Autoplaying video
- None anywhere on the site
The bit that is easy to get wrong.
A great many sites implement prefers-reduced-motion by disabling their scroll library and stopping there. What that produces is a page where half the content is still sitting at opacity zero waiting for a reveal that will never fire.
The fix is unglamorous: every element that a runtime will animate has to be visible and correctly positioned before that runtime touches it, and the runtime hides it only after confirming it is going to animate it.
It means the page flashes fully-formed for a frame on a fast connection with motion enabled, which is a cosmetic cost. The alternative is a blank page for anybody whose JavaScript failed, which is not cosmetic at all.
And the same applies to the seasonal layer
None of the live inputs animate anything. The palette swap happens server-side before first paint, the ticker does not scroll under reduced motion, and no value on this site ever counts up, flashes or pulses to draw attention to itself.
Day one
Not a retrofit.
A storyboard
What a scrub sequence becomes.
Nothing hidden
No content lives only inside an animation.
Identity survives
It appears. It simply does not perform.
The full accessibility position.
Contrast, focus, targets, labelling and what this build knows it has not done.