diff --git a/app/globals.css b/app/globals.css index 86c5a53..1f08a23 100644 --- a/app/globals.css +++ b/app/globals.css @@ -78,3 +78,19 @@ body { background-position: 200% center; } } + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } + + .gradient-text-animated { + animation: none !important; + background-position: 0% center; + } +} diff --git a/components/landing/hero.tsx b/components/landing/hero.tsx index fb18182..3e8c73e 100644 --- a/components/landing/hero.tsx +++ b/components/landing/hero.tsx @@ -2,10 +2,11 @@ import { ArrowRight } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { MagneticWrap } from "@/components/landing/motion"; +import { MagneticWrap, usePrefersReducedMotion } from "@/components/landing/motion"; import { ScorePreview } from "@/components/landing/score-preview"; import { AnimatedCounter, AnimatedGradientText } from "@/components/landing/text-effects"; import { getDappUrl } from "@/lib/site"; +import { cn } from "@/lib/utils"; const fadeUp = (delay: number) => ({ @@ -14,6 +15,11 @@ const fadeUp = (delay: number) => }); export function Hero() { + const prefersReducedMotion = usePrefersReducedMotion(); + const entranceClass = prefersReducedMotion + ? "opacity-100" + : "animate-slide-up opacity-0"; + return (
@@ -29,10 +35,18 @@ export function Hero() {
- + Zero-Knowledge ยท On-chain only @@ -41,16 +55,16 @@ export function Hero() {

YOUR WALLET IS YOUR CREDIT. @@ -58,8 +72,11 @@ export function Hero() {

All-in-one portable credit scoring for Stellar. Discover your score, access better rates, and activate your on-chain reputation @@ -67,16 +84,22 @@ export function Hero() {

No banks. No forms. No gatekeepers. Built from real on-chain proof, not paperwork.

0-850
@@ -142,8 +168,11 @@ export function Hero() {
-
-
+
+
diff --git a/components/landing/motion.tsx b/components/landing/motion.tsx index 23366cc..eb4fc81 100644 --- a/components/landing/motion.tsx +++ b/components/landing/motion.tsx @@ -1,18 +1,35 @@ "use client"; -import { useEffect, useRef, type ReactNode } from "react"; +import { useEffect, useRef, useState, type ReactNode } from "react"; import { cn } from "@/lib/utils"; +export function usePrefersReducedMotion() { + const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); + + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); + const handleChange = () => setPrefersReducedMotion(mediaQuery.matches); + + handleChange(); + mediaQuery.addEventListener("change", handleChange); + + return () => mediaQuery.removeEventListener("change", handleChange); + }, []); + + return prefersReducedMotion; +} + interface MouseParallaxBgProps { className?: string; } export function MouseParallaxBg({ className }: MouseParallaxBgProps) { const ref = useRef(null); + const prefersReducedMotion = usePrefersReducedMotion(); useEffect(() => { const el = ref.current; - if (!el) return; + if (!el || prefersReducedMotion) return; function onMove(e: MouseEvent) { if (!el) return; @@ -24,7 +41,7 @@ export function MouseParallaxBg({ className }: MouseParallaxBgProps) { window.addEventListener("mousemove", onMove, { passive: true }); return () => window.removeEventListener("mousemove", onMove); - }, []); + }, [prefersReducedMotion]); return (
- + {!prefersReducedMotion && }
); @@ -117,8 +146,10 @@ interface TiltCardProps { export function TiltCard({ children, className }: TiltCardProps) { const ref = useRef(null); + const prefersReducedMotion = usePrefersReducedMotion(); function handleMove(e: React.MouseEvent) { + if (prefersReducedMotion) return; const el = ref.current; if (!el) return; const rect = el.getBoundingClientRect(); @@ -128,6 +159,7 @@ export function TiltCard({ children, className }: TiltCardProps) { } function handleLeave() { + if (prefersReducedMotion) return; const el = ref.current; if (!el) return; el.style.transform = @@ -139,7 +171,11 @@ export function TiltCard({ children, className }: TiltCardProps) { ref={ref} onMouseMove={handleMove} onMouseLeave={handleLeave} - className={cn("transition-transform duration-200 ease-out will-change-transform", className)} + className={cn( + "transition-transform duration-200 ease-out will-change-transform", + prefersReducedMotion && "transition-none will-change-auto", + className + )} > {children}
@@ -158,8 +194,10 @@ export function MagneticWrap({ strength = 0.25, }: MagneticWrapProps) { const ref = useRef(null); + const prefersReducedMotion = usePrefersReducedMotion(); function handleMove(e: React.MouseEvent) { + if (prefersReducedMotion) return; const el = ref.current; if (!el) return; const rect = el.getBoundingClientRect(); @@ -169,6 +207,7 @@ export function MagneticWrap({ } function handleLeave() { + if (prefersReducedMotion) return; const el = ref.current; if (!el) return; el.style.transform = "translate(0px, 0px)"; @@ -179,7 +218,11 @@ export function MagneticWrap({ ref={ref} onMouseMove={handleMove} onMouseLeave={handleLeave} - className={cn("inline-block transition-transform duration-300 ease-out", className)} + className={cn( + "inline-block transition-transform duration-300 ease-out", + prefersReducedMotion && "transition-none", + className + )} > {children}
diff --git a/components/landing/reveal.tsx b/components/landing/reveal.tsx index 37b2c85..5a45d0b 100644 --- a/components/landing/reveal.tsx +++ b/components/landing/reveal.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState, type ReactNode } from "react"; import { cn } from "@/lib/utils"; +import { usePrefersReducedMotion } from "@/components/landing/motion"; type RevealVariant = "up" | "down" | "left" | "right" | "scale" | "blur"; @@ -38,8 +39,14 @@ export function Reveal({ }: RevealProps) { const ref = useRef(null); const [visible, setVisible] = useState(false); + const prefersReducedMotion = usePrefersReducedMotion(); useEffect(() => { + if (prefersReducedMotion) { + setVisible(true); + return; + } + const el = ref.current; if (!el) return; @@ -55,17 +62,22 @@ export function Reveal({ observer.observe(el); return () => observer.disconnect(); - }, []); + }, [prefersReducedMotion]); + + const isVisible = visible || prefersReducedMotion; return (
{children}
@@ -83,8 +95,14 @@ export function StaggerChildren({ }) { const ref = useRef(null); const [visible, setVisible] = useState(false); + const prefersReducedMotion = usePrefersReducedMotion(); useEffect(() => { + if (prefersReducedMotion) { + setVisible(true); + return; + } + const el = ref.current; if (!el) return; @@ -100,7 +118,9 @@ export function StaggerChildren({ observer.observe(el); return () => observer.disconnect(); - }, []); + }, [prefersReducedMotion]); + + const isVisible = visible || prefersReducedMotion; return (
@@ -110,12 +130,14 @@ export function StaggerChildren({ key={i} className={cn( "transition-all duration-700 ease-out", - visible + prefersReducedMotion && "transition-none", + isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-6" )} style={{ - transitionDelay: visible ? `${i * staggerMs}ms` : "0ms", + transitionDelay: + isVisible && !prefersReducedMotion ? `${i * staggerMs}ms` : "0ms", }} > {child} diff --git a/components/landing/text-effects.tsx b/components/landing/text-effects.tsx index 325e5e7..6247e07 100644 --- a/components/landing/text-effects.tsx +++ b/components/landing/text-effects.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; +import { usePrefersReducedMotion } from "@/components/landing/motion"; interface AnimatedCounterProps { value: number; @@ -21,8 +22,14 @@ export function AnimatedCounter({ const ref = useRef(null); const [display, setDisplay] = useState(0); const [started, setStarted] = useState(false); + const prefersReducedMotion = usePrefersReducedMotion(); useEffect(() => { + if (prefersReducedMotion) { + setStarted(true); + return; + } + const el = ref.current; if (!el) return; @@ -38,11 +45,16 @@ export function AnimatedCounter({ observer.observe(el); return () => observer.disconnect(); - }, []); + }, [prefersReducedMotion]); useEffect(() => { if (!started) return; + if (prefersReducedMotion) { + setDisplay(value); + return; + } + let frame: number; const start = performance.now(); @@ -55,7 +67,7 @@ export function AnimatedCounter({ frame = requestAnimationFrame(tick); return () => cancelAnimationFrame(frame); - }, [started, value, duration]); + }, [started, value, duration, prefersReducedMotion]); return ( @@ -79,18 +91,21 @@ export function StaggerLines({ lineClassName, delayStep = 100, }: StaggerLinesProps) { + const prefersReducedMotion = usePrefersReducedMotion(); + return ( {lines.map((line, i) => ( {line} @@ -108,7 +123,16 @@ export function AnimatedGradientText({ children: React.ReactNode; className?: string; }) { + const prefersReducedMotion = usePrefersReducedMotion(); + return ( - {children} + + {children} + ); }