fix(motion): honour reduced motion without stranding content - #24
Merged
Conversation
Reveal branched on useReducedMotion at render, so the server (which cannot read the preference) emitted framer's hidden state as inline styles while the client rendered a bare div. React does not patch up inline-style mismatches, leaving every Reveal stuck at opacity 0: all four routes rendered blank below the hero under reduced motion. Reveal now always animates and only swaps its variant, since the animation is the one thing that clears the SSR start state. The same rule applied elsewhere: keep the fade, drop the travel. - reveal/motion: revealItemReduced snaps the rise and de-blur, fades opacity; both variants share one hidden state so the markup hydrates identically - marquee: a paused drift inside overflow-hidden put every entry past the edge out of reach, so the row becomes a plain scroller and drops its seam duplicate - reticle: the outer logo marks fade in place instead of travelling their height - drawer: vaul animates the panel from its own injected stylesheet, beyond the reach of motion-reduce utilities, so the slide keyframes swap for a fade
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
With
prefers-reduced-motion: reduce, the about page rendered blank below the hero. The cause was not about-specific:Revealis used on all four routes, so home, works, experience and work details were all affected.Revealbranched onuseReducedMotion()at render time. That hook cannot run on the server, so SSR always emitted the animated branch, with framer serialisinghiddeninto inlinestyle="opacity:0; filter:blur(8px); transform:translateY(24px)". The client, under reduced motion, rendered a plain stylelessdiv. React reconciles the element but does not patch up inline-style mismatches (it says so in the hydration warning), so the server'sopacity: 0stayed in the DOM permanently.The fix keeps server and client markup identical and varies only the transition. Under reduced motion the element must still animate: the animation is the only thing that clears the SSR-rendered start state.
What
The same principle throughout: keep the fade, drop the travel. Opacity carries no vestibular cost; the travel is what reduced motion is asking about.
constants/motion.tsrevealItemReducedsnaps the rise and de-blur toduration: 0and fades opacity; both variants share onerevealHiddenstate so markup hydrates identicallyatoms/reveal.tsxvariantsatoms/marquee.tsxmotion-reduce:pausedfroze the track insideoverflow-hidden, putting every entry past the right edge permanently out of reach. Becomes a plain scroller and drops its seam duplicateatoms/reticle.tsxthemes/theme.cssmotion-reduce:utilities and with no prop to disable it, so its slide keyframes swap for a fadedocs/02-prd/03-solution.mdAccordions are deliberately untouched: 0.2s, user-initiated, and the height growth is the disclosure feedback.
Verification
Driven in Chromium via Playwright against both
reducedMotion: reduceandno-preference:Revealwrappers left below opacity 1 on/about(previously all of them).reduce:overflow-x: auto, animation off, duplicatedisplay: none, 944px of reachable scroll. Underno-preference: unchanged.reduce: all three logo groups onfade-in. Underno-preference:boot-down/fade-in/boot-upstagger intact.pnpm type:checkandpnpm lint:checkclean.Not covered
tabIndex. Chrome 127+ and Firefox make scroll containers keyboard-focusable automatically, but that is not universal, and adding it conditionally needs a client component.atoms/counter.tsxuses the same branch-on-reduced shape. Far less severe (text mismatches are re-rendered by React rather than stranded), but it is the same latent pattern.