diff --git a/site/app/components/before-after.tsx b/site/app/components/before-after.tsx index b5042d1..afba28c 100644 --- a/site/app/components/before-after.tsx +++ b/site/app/components/before-after.tsx @@ -1,6 +1,6 @@ "use client"; -import { AnimatePresence, motion, useInView } from "motion/react"; +import { AnimatePresence, motion, useInView, useReducedMotion } from "motion/react"; import { useEffect, useRef, useState } from "react"; import { ScrollReveal } from "./scroll-reveal"; @@ -217,7 +217,13 @@ function CodePanel({ ))} -
+				
 					
 						{lines.map((line, i) => (
 							// biome-ignore lint/suspicious/noArrayIndexKey: static code lines
@@ -269,6 +275,25 @@ export function BeforeAfter() {
 	const sectionRef = useRef(null);
 	const inView = useInView(sectionRef, { once: true, amount: 0.2 });
 	const [showAfter, setShowAfter] = useState(false);
+	/*
+	 * The panel swap is a full-panel scale plus a brightness flash, and it also
+	 * fires on its own after 2s — motion the reader never asked for. Under
+	 * prefers-reduced-motion the duration drops to 0 so the panels still swap,
+	 * they just do not travel.
+	 *
+	 * useReducedMotion rather than matchMedia in render: matchMedia does not exist
+	 * on the server, so reading it during render would throw there and hydrate
+	 * mismatched here.
+	 *
+	 * Two things the hook does NOT do, in motion@12: it does not react to the OS
+	 * setting changing mid-session (it snapshots once via useState — upstream's own
+	 * TODO), and it returns null on the server rather than a boolean. Neither bites:
+	 * null is falsy, so SSR renders the full duration, and `transition` is never
+	 * serialized into the DOM, so there is nothing for hydration to disagree about.
+	 * If mid-session reactivity is ever wanted, it needs a real listener — do not
+	 * assume the hook already provides one.
+	 */
+	const shouldReduceMotion = useReducedMotion();
 
 	// Auto-flip from Before to After after 2s of being in view
 	useEffect(() => {
@@ -278,7 +303,7 @@ export function BeforeAfter() {
 	}, [inView]);
 
 	return (
-		
+
{/* Section header */}
@@ -304,11 +329,17 @@ export function BeforeAfter() { {/* Animated toggle pills */} + {/* + * The pills are a two-state toggle, not navigation: without aria-pressed a + * screen reader announces two ordinary buttons and never says which panel + * is currently on screen — including after the 2s auto-flip moves it. + */}