Next.js View Transitions and reduced motion: what the documented snippet covers
The reduced-motion block in the Next.js View Transitions guide governs the ::view-transition pseudo-element tree — and only that. It is a correct, well-placed snippet, and if you ship it your route transitions behave properly for someone who has asked for less motion. What it does not touch is every other animation in the same application: the skeleton pulse under the Suspense boundary, the spinner in the header, the hover transitions on your cards. Those live in ordinary CSS, outside the pseudo-element tree, and they need their own guard. This guide shows the boundary precisely, using the official demo as the measured example — including one finding where our own scanner got it wrong.
What the Next.js guide actually documents
The Next.js View Transitions guide has a section called "Respecting reduced motion", and it names the risk correctly:
"Directional slides simulate physical movement across the viewport. This is the most common trigger for motion sensitivity. Morphs, reveals, and crossfades carry less risk since they affect smaller areas or rely on opacity rather than position."
— Next.js, Designing view transitions
The snippet it recommends is this, and it is the right shape:
@media (prefers-reduced-motion: reduce) {
::view-transition-old(*),
::view-transition-new(*),
::view-transition-group(*) {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
}
The guide is also candid that this is the blunt version: it notes that "a more refined approach would preserve crossfades and opacity transitions while removing positional movement". That refinement is worth doing — a crossfade carries the "the content changed" signal that a hard cut throws away.
Where the boundary falls
Three selectors, one tree. ::view-transition-old(), ::view-transition-new() and ::view-transition-group() address the snapshot pseudo-elements the browser builds for the duration of a transition. They are not descendant selectors and they do not cascade into your document. Anything animated on a real DOM element — a class, a keyframe, a transition utility — is a separate rule in a separate part of the stylesheet, and the block above never reaches it.
That matters because a View Transitions app is rarely all view transitions. The same guide's Step 2 puts a skeleton behind a <Suspense> boundary while data loads; skeletons are conventionally a pulsing element. The pulse is CSS animation on a real node. It runs during the load, under the reduced-motion preference, unaffected by the pseudo-element block.
What we measured on the official demo
On 2026-08-29 we ran the free motion check against the reference implementation linked from the guide, react-view-transitions-demo.labs.vercel.dev. Result: score 55/100, 10 distinct motion root causes across 18 occurrences, plus the disclosure "Runtime motion detected (requestAnimationFrame-Loop): not audited (V2)."
We then read the deployed stylesheet directly to check the findings by hand. Two things came back, and they point in opposite directions.
The true finding: the skeleton pulse is outside the guard
The deployed CSS contains .animate-pulse{animation:var(--animate-pulse)}, where --animate-pulse resolves to pulse 2s cubic-bezier(.4, 0, .6, 1) infinite. It is a Tailwind utility, on a real element, and it is not inside the prefers-reduced-motion block — which contains only the three ::view-transition-* selectors. So on this page the route transitions honour the preference and the loading skeleton does not. That is the boundary, in one file, in a demo written by the people who wrote the guide. It is not carelessness; it is what happens when the guard is scoped to the feature the guide is about.
The false finding: our scan flagged rules that were guarded
Our scan also reported ::view-transition-old(.nav-forward), ::view-transition-old(.slide-down), ::view-transition-old(.fade-out) and ::view-transition-image-pair(.morph) under "Motion without prefers-reduced-motion guard". Reading the deployed CSS, that is wrong: the wildcard block is present and does cover them. Our matcher did not connect a guard written as ::view-transition-old(*) { animation-duration: 0s } to the named rules it neutralises.
::view-transition-* selectors as a list to verify by hand: search the stylesheet for a prefers-reduced-motion block with wildcard view-transition selectors before you change anything. The .animate-pulse-class findings — ordinary animation on real elements — are the ones to act on.
Which WCAG criterion applies to a view transition?
Normally 2.3.3, not 2.2.2. This is worth getting right, because the two criteria sit at opposite ends of the conformance scale and teams routinely file view transitions under the wrong one.
| Criterion | Level | Trigger it covers | Applies to a route transition? |
|---|---|---|---|
| 2.2.2 Pause, Stop, Hide | A | Motion that starts automatically, runs over five seconds, alongside other content | Usually no — the user clicked a link, and it ends in well under a second |
| 2.3.3 Animation from Interactions | AAA | Motion triggered by user interaction, which should be disableable | Yes — this is exactly the case it describes |
Neither criterion is fully machine-checkable: whether motion is "essential" is a human judgment. This is a mapping, not a conformance verdict.
So the reduced-motion block is not optional polish — it is the whole of the accessibility story for the transition itself, and it sits on the best-practice track rather than the Level A one. The Level A exposure in a View Transitions app comes from the other motion: an infinite skeleton pulse that keeps running while a slow request hangs, a spinner in a populated page, an autoplaying hero.
The version we would ship
Two blocks, not one: the guide's snippet for the transition tree, refined to keep the crossfade, plus a floor for everything else.
@media (prefers-reduced-motion: reduce) {
/* 1. The transition tree. Keep the crossfade, drop the movement:
a hard cut loses the "content changed" signal. */
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 120ms !important;
animation-delay: 0s !important;
animation-timing-function: linear !important;
}
/* Slides are the positional case the Next.js guide names. Remove them. */
::view-transition-old(.nav-forward),
::view-transition-new(.nav-forward),
::view-transition-old(.nav-back),
::view-transition-new(.nav-back),
::view-transition-old(.slide-down),
::view-transition-new(.slide-up) {
animation-name: none !important;
}
/* 2. Everything else in the app — skeletons, spinners, hover states. */
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
The second block is deliberately blunt; it is the floor, not the design. Where a specific animation carries information, write it a reduced variant instead of letting the blanket rule flatten it — the same principle as the reduced ScrollTrigger variants in our GSAP guide.
What this does not cover
- Anything React drives at runtime. The
ViewTransitioncomponent itself is CSS-driven, but the surrounding app usually is not. ArequestAnimationFrameloop, the Web Animations API, or Motion for React are all invisible to a CSS scan — the demo's own result disclosed exactly that. - Browser support differences. Where the View Transitions API is unsupported the navigation simply does not animate, which is safe by default — but it also means your reduced-motion CSS is untested in that path.
- Whether the transition is a good idea. A 400ms directional slide on every navigation is a design decision no criterion settles. The guide's own judgment — that positional movement is the highest-risk kind — is the right starting point.
- Our scanner's wildcard-guard gap, described above. Until that is fixed, hand-verify
::view-transition-*findings.
The free motion check reads a page's linked CSS files and inline style blocks and reports the failing patterns it can see, each with a fix. It does not certify a page and it is not a legal determination. The full manual process is in how to verify web motion accessibility.
FAQ
Do Next.js View Transitions respect prefers-reduced-motion automatically?
No. The browser runs whatever animation your CSS declares on the ::view-transition pseudo-elements. The Next.js guide documents a reduced-motion block you add yourself, which sets animation-duration and animation-delay to 0s so content swaps instantly. Without that block, the transitions play regardless of the preference.
Which WCAG criterion applies to a view transition?
Normally WCAG 2.3.3 Animation from Interactions (Level AAA), because a view transition is triggered by a navigation the user initiated and ends on its own in a few hundred milliseconds. WCAG 2.2.2 Pause, Stop, Hide (Level A) requires motion that starts automatically and runs longer than five seconds, which a route transition does not.
Does the reduced-motion snippet cover the rest of the app's animation?
No. The selectors in the documented block are ::view-transition-old(*), ::view-transition-new(*) and ::view-transition-group(*) — the pseudo-element tree the browser builds for the transition. Skeleton pulses, spinners, hover transitions and any other ordinary CSS animation in the same app are untouched by it and need their own guard.
Can a static CSS scan verify view transitions?
Only partially, and it can be wrong in both directions. A scan reads the CSS, so it sees the ::view-transition rules — but our own scan flagged rules that were in fact guarded by a wildcard block using animation-duration: 0s, which it did not match to the named selectors. It also cannot see anything React drives at runtime. Treat scan findings on a View Transitions page as a list to check by hand.
The free motion check scans a URL's CSS against WCAG 2.2.2 and 2.3.3 and returns every finding with its fix — including the skeletons and spinners that sit outside your view-transition guard. No signup, nothing stored.
Run the free motion checkOne page tells you. A dated write‑up is what you hand to a client or a boss: the $29 Motion Report cites every candidate to WCAG 2.2.2 / 2.3.3 and ranks your score against 196 AI‑built pages — read a real one first. Fourteen‑day refund, no questions asked.
Shipping Next.js sites for clients? The agency motion handoff check →
MotionSpec checks and compiles UI animation for AI-generated web apps — it does not generate AI video.