Animations
Motion is part of the Lerpa UI house style — but it always degrades gracefully. Components use Framer Motionand gate every animation behind the user's reduced-motion preference.
The pattern
Read the preference with usePrefersReducedMotion and branch your variants and transitions. When motion is reduced, the element snaps to its final state with duration: 0 instead of animating.
"use client";
import { motion } from "framer-motion";
import { usePrefersReducedMotion } from "@lerpa/ui";
export function Reveal({ children }: { children: React.ReactNode }) {
const reduced = usePrefersReducedMotion();
return (
<motion.div
initial={reduced ? { opacity: 1 } : { opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "0px 0px -12% 0px" }}
transition={reduced ? { duration: 0 } : { duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
>
{children}
</motion.div>
);
}Staggered reveals
For lists, drive children off the parent with a small per-item delay — and clamp the total so long lists don't lag. The same reduced-motion branch zeroes the delay.
const variants = {
hidden: reduced ? { opacity: 1 } : { opacity: 0, y: 20 },
visible: (i: number) => ({
opacity: 1,
y: 0,
transition: reduced
? { duration: 0 }
: { duration: 0.45, delay: Math.min(i * 0.04, 0.2) },
}),
};App-wide reduced motion
At the app level you can wrap your tree in Framer Motion's MotionConfig so animations respect the system setting globally — the docs site does exactly this:
import { MotionConfig } from "framer-motion";
export default function Layout({ children }) {
return <MotionConfig reducedMotion="user">{children}</MotionConfig>;
}Guidelines
- Never animate without a fallback. Every animated component branches on reduced motion.
- Keep durations short. Entrances are typically 0.3–0.6s with an ease-out curve.
- Animate transforms and opacity rather than layout properties, to stay on the compositor and avoid jank.
Motion accessibility is covered further in the accessibility guide.