-
Notifications
You must be signed in to change notification settings - Fork 2.5k
fix(web): respect prefers-reduced-motion in AnimatedBackground #8329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -225,8 +225,9 @@ export function AnimatedBackground() { | |||||||||||||||||
| connectParticles() | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Animation loop. | ||||||||||||||||||
| let animationId: number | ||||||||||||||||||
| // Animation loop and reduced-motion handling. | ||||||||||||||||||
| let animationId: number | undefined | ||||||||||||||||||
| let isAnimating = false | ||||||||||||||||||
|
|
||||||||||||||||||
| // Target position for smooth following. | ||||||||||||||||||
| let targetX = canvas.width * 0.2 | ||||||||||||||||||
|
|
@@ -265,14 +266,56 @@ export function AnimatedBackground() { | |||||||||||||||||
| drawGrid() | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| animate() | ||||||||||||||||||
| const startAnimation = () => { | ||||||||||||||||||
| if (isAnimating) return | ||||||||||||||||||
| isAnimating = true | ||||||||||||||||||
| window.addEventListener("mousemove", handleMouseMove) | ||||||||||||||||||
| animate() | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const stopAnimation = () => { | ||||||||||||||||||
| if (!isAnimating) return | ||||||||||||||||||
| isAnimating = false | ||||||||||||||||||
| window.removeEventListener("mousemove", handleMouseMove) | ||||||||||||||||||
| if (animationId !== undefined) cancelAnimationFrame(animationId) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Respect user preference for reduced motion | ||||||||||||||||||
| const mediaQuery = | ||||||||||||||||||
| typeof window !== "undefined" && "matchMedia" in window | ||||||||||||||||||
| ? window.matchMedia("(prefers-reduced-motion: reduce)") | ||||||||||||||||||
| : null | ||||||||||||||||||
|
|
||||||||||||||||||
| const onMotionPreferenceChange = (e: MediaQueryListEvent) => { | ||||||||||||||||||
| if (e.matches) { | ||||||||||||||||||
| // User prefers reduced motion now | ||||||||||||||||||
| stopAnimation() | ||||||||||||||||||
| // Render a single static frame | ||||||||||||||||||
| drawGrid() | ||||||||||||||||||
| } else { | ||||||||||||||||||
| // User allows motion | ||||||||||||||||||
| startAnimation() | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| window.addEventListener("mousemove", handleMouseMove) | ||||||||||||||||||
| if (mediaQuery) { | ||||||||||||||||||
| if (mediaQuery.matches) { | ||||||||||||||||||
| // Reduced motion: render one static frame | ||||||||||||||||||
| drawGrid() | ||||||||||||||||||
| } else { | ||||||||||||||||||
| startAnimation() | ||||||||||||||||||
| } | ||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Older Safari uses the legacy addListener API for MediaQueryList. Add a runtime fallback so preference changes are handled across browsers.
Suggested change
|
||||||||||||||||||
| mediaQuery.addEventListener("change", onMotionPreferenceChange) | ||||||||||||||||||
| } else { | ||||||||||||||||||
| // Fallback if matchMedia is unavailable | ||||||||||||||||||
| startAnimation() | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| return () => { | ||||||||||||||||||
| window.removeEventListener("resize", resizeCanvas) | ||||||||||||||||||
| window.removeEventListener("mousemove", handleMouseMove) | ||||||||||||||||||
| cancelAnimationFrame(animationId) | ||||||||||||||||||
| // Ensure animation and listeners are stopped | ||||||||||||||||||
| stopAnimation() | ||||||||||||||||||
| if (mediaQuery) mediaQuery.removeEventListener("change", onMotionPreferenceChange) | ||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Mirror the addListener fallback on cleanup to ensure the change listener is properly removed in older Safari.
Suggested change
|
||||||||||||||||||
| } | ||||||||||||||||||
| }, []) | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: Minor: after cancelAnimationFrame, reset animationId to undefined to avoid accidental double-cancel or stale ids after toggling animation state.