Motion should clarify, not decorate. This guide covers principles, durations, easing, accessibility, and production patterns for Next.js with both CSS and Framer Motion.
Related: Web Design Best Practices · Web Application Design · UI Libraries
Principles
- Purposeful: communicate state change (open/close, success, progress).
- Brief: 150–240ms for small UI, 300–400ms for overlays.
- Directional: motion should match spatial model (slide from source).
- Consistent: same components, same durations and easings.
- Respectful: honor
prefers-reduced-motionwith non-animated fallbacks.
Easing Recipes (feel right)
- Entrance:
cubic-bezier(0.2, 0.8, 0.2, 1)(easeOut-ish) - Exit:
cubic-bezier(0.4, 0.0, 1, 1)(easeIn-ish) - Emphasis: Spring with low stiffness, high damping
:root {
--ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--dur-1: 160ms;
--dur-2: 220ms;
--dur-3: 320ms;
}CSS-Only Patterns (fast, simple)
.modal-enter {
opacity: 0;
transform: translateY(8px);
}
.modal-enter-active {
opacity: 1;
transform: translateY(0);
transition:
opacity var(--dur-2) var(--ease-out),
transform var(--dur-2) var(--ease-out);
}
.modal-exit {
opacity: 1;
transform: translateY(0);
}
.modal-exit-active {
opacity: 0;
transform: translateY(8px);
transition:
opacity var(--dur-2) var(--ease-in),
transform var(--dur-2) var(--ease-in);
}Reduced motion
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}Framer Motion (App Router)
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { usePathname } from "next/navigation";
export function RouteTransition({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.22, ease: [0.2, 0.8, 0.2, 1] }}
>
{children}
</motion.div>
</AnimatePresence>
);
}Micro-interactions
"use client";
import { motion } from "framer-motion";
export function TapButton({ children }) {
return (
<motion.button
whileTap={{ scale: 0.96 }}
whileHover={{ y: -1 }}
transition={{ type: "spring", stiffness: 500, damping: 40 }}
className="rounded-md bg-neutral-900 px-4 py-2 text-white"
>
{children}
</motion.button>
);
}Performance Guardrails
- Animate transform/opacity; avoid width/height/top/left.
- Avoid animating large shadows or blurs.
- Use
will-change: transformsparingly for complex elements. - Limit concurrent animations; stagger for readability.
- Consider motion-safe loading (skeletons, progressive disclosure).
Testing & Accessibility
- Verify keyboard + screen reader behavior doesn’t regress during motion.
- Stop non-essential animations if tab is unfocused.
- Provide “Turn off animations” in app settings if motion is a core theme.
Checklist
- Motion clarifies intent
- Durations & easings consistent
- Reduced motion supported
- No layout thrash
- Measured in the field (INP/paint timings)
Keep exploring: Web Design Best Practices · Web Application Design · UI Libraries

