Skip to content

Commit 66f8414

Browse files
committed
fix(accessibility): respect reduced motion in animated components
1 parent 880de31 commit 66f8414

4 files changed

Lines changed: 69 additions & 19 deletions

File tree

src/components/common/preview-video.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,13 @@ export function PreviewVideo({
6464
card.addEventListener("focusout", pause);
6565
return () => {
6666
io.disconnect();
67+
video.pause();
6768
card.removeEventListener("pointerenter", play);
6869
card.removeEventListener("pointerleave", pause);
6970
card.removeEventListener("focusin", play);
7071
card.removeEventListener("focusout", pause);
7172
};
72-
}, []);
73+
}, [src]);
7374

7475
return (
7576
<video

src/components/landing/hero-reel.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export function HeroReel({
9393
if (!holder) return;
9494
const components = itemsRef.current;
9595
const count = components.length;
96+
if (count === 0) return;
9697
const step = (Math.PI * 2) / count;
9798
const fitMaxHalfHeight = step * FIT_MAX_RATIO;
9899
const wideHalfHeight = step * WIDE_RATIO;
@@ -214,7 +215,9 @@ export function HeroReel({
214215
const resizeObserver = new ResizeObserver(resize);
215216
resizeObserver.observe(holder);
216217

218+
let disposed = false;
217219
document.fonts?.ready.then(() => {
220+
if (disposed) return;
218221
rows.forEach((row, i) => {
219222
row.aspect = drawLabel(
220223
row.texture.image as HTMLCanvasElement,
@@ -255,12 +258,14 @@ export function HeroReel({
255258
"(prefers-reduced-motion: reduce)",
256259
).matches;
257260
let tick = 0;
258-
const interval = window.setInterval(() => {
259-
if (!pausedRef.current && !reducedMotion && !document.hidden) {
260-
tick += 1;
261-
onActiveChangeRef.current?.(tick % count);
262-
}
263-
}, 5000);
261+
const interval = reducedMotion
262+
? null
263+
: window.setInterval(() => {
264+
if (!pausedRef.current && !document.hidden) {
265+
tick += 1;
266+
onActiveChangeRef.current?.(tick % count);
267+
}
268+
}, 5000);
264269

265270
let rotation = 0;
266271
let push = 0;
@@ -306,6 +311,10 @@ export function HeroReel({
306311
row.mesh.renderOrder = Math.round(row.alpha * 100);
307312
});
308313
renderer.render(scene, camera);
314+
if (reducedMotion) {
315+
running = false;
316+
return;
317+
}
309318
raf = requestAnimationFrame(animate);
310319
};
311320

@@ -344,8 +353,9 @@ export function HeroReel({
344353
start();
345354

346355
return () => {
356+
disposed = true;
347357
stop();
348-
window.clearInterval(interval);
358+
if (interval !== null) window.clearInterval(interval);
349359
themeObserver.disconnect();
350360
resizeObserver.disconnect();
351361
viewObserver.disconnect();

src/components/landing/hero-reveal.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ export function HeroReveal({
3535
let last = performance.now();
3636
let inView = true;
3737
let running = false;
38+
const reducedMotion = window.matchMedia(
39+
"(prefers-reduced-motion: reduce)",
40+
).matches;
41+
let hostRect = host.getBoundingClientRect();
42+
43+
const updateHostRect = () => {
44+
hostRect = host.getBoundingClientRect();
45+
};
46+
47+
const resizeObserver = new ResizeObserver(updateHostRect);
48+
resizeObserver.observe(host);
49+
window.addEventListener("resize", updateHostRect);
50+
window.addEventListener("scroll", updateHostRect, { passive: true });
3851

3952
const onMove = (event: PointerEvent) => {
4053
if (event.isTrusted) userActive = true;
@@ -58,20 +71,19 @@ export function HeroReveal({
5871
0.5 + 0.34 * Math.sin(t * 0.37) + 0.14 * Math.sin(t * 0.93 + 1.7);
5972
const ny =
6073
0.5 + 0.3 * Math.sin(t * 0.53 + 0.8) + 0.14 * Math.sin(t * 1.19 + 4.1);
61-
const rect = host.getBoundingClientRect();
6274
target.dispatchEvent(
6375
new PointerEvent("pointermove", {
6476
bubbles: true,
65-
clientX: rect.left + rect.width * nx,
66-
clientY: rect.top + rect.height * ny,
77+
clientX: hostRect.left + hostRect.width * nx,
78+
clientY: hostRect.top + hostRect.height * ny,
6779
}),
6880
);
6981
}
7082
raf = requestAnimationFrame(tick);
7183
};
7284

7385
const start = () => {
74-
if (running || !inView || document.hidden) return;
86+
if (reducedMotion || running || !inView || document.hidden) return;
7587
running = true;
7688
last = performance.now();
7789
raf = requestAnimationFrame(tick);
@@ -107,7 +119,10 @@ export function HeroReveal({
107119
return () => {
108120
stop();
109121
observer.disconnect();
122+
resizeObserver.disconnect();
110123
document.removeEventListener("visibilitychange", onVisibilityChange);
124+
window.removeEventListener("resize", updateHostRect);
125+
window.removeEventListener("scroll", updateHostRect);
111126
host.removeEventListener("pointermove", onMove, true);
112127
host.removeEventListener("pointerleave", onLeave, true);
113128
};

src/demos/demo-image-cycler.tsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,39 @@ export function DemoImageCycler({
4545
interval?: number;
4646
}) {
4747
const shouldReduceMotion = useReducedMotion();
48-
const [active, setActive] = useState(0);
48+
const imageCount = images.length;
49+
const [activeIndex, setActiveIndex] = useState(0);
4950

5051
useEffect(() => {
51-
const id = setInterval(
52-
() => setActive((prev) => (prev + 1) % images.length),
53-
interval,
54-
);
55-
return () => clearInterval(id);
56-
}, [images.length, interval]);
52+
if (shouldReduceMotion || imageCount < 2 || interval <= 0) return;
53+
54+
let timer: ReturnType<typeof setInterval> | null = null;
55+
const stop = () => {
56+
if (timer === null) return;
57+
clearInterval(timer);
58+
timer = null;
59+
};
60+
const start = () => {
61+
if (timer !== null || document.hidden) return;
62+
timer = setInterval(
63+
() => setActiveIndex((prev) => (prev + 1) % imageCount),
64+
interval,
65+
);
66+
};
67+
const onVisibilityChange = () => {
68+
if (document.hidden) stop();
69+
else start();
70+
};
71+
72+
start();
73+
document.addEventListener("visibilitychange", onVisibilityChange);
74+
return () => {
75+
stop();
76+
document.removeEventListener("visibilitychange", onVisibilityChange);
77+
};
78+
}, [imageCount, interval, shouldReduceMotion]);
79+
80+
const active = imageCount === 0 ? 0 : activeIndex % imageCount;
5781

5882
return (
5983
<div className="relative aspect-video w-full overflow-hidden">

0 commit comments

Comments
 (0)