Section Motion Animation Primitive
section.tsxTSX
section.tsx
import {
motion,
type Transition,
useReducedMotion,
type Variants,
} from "motion/react";
import type { ComponentProps, ReactNode } from "react";
import { cn } from "@/lib/utils";
type TVariant =
| "fade"
| "none"
| "slide-up"
| "slide-down"
| "slide-left"
| "slide-right"
| "blur"
| "blur-up"
| "blur-down"
| "blur-left"
| "blur-right"
| "scale"
| "scale-up"
| "scale-down"
| "zoom-in"
| "zoom-out"
| "rotate-in"
| "rotate-in-left"
| "rotate-in-right"
| "bounce"
| "bounce-up"
| "bounce-down"
| "bounce-left"
| "bounce-right"
| "flip-x"
| "flip-y"
| "roll-left"
| "roll-right"
| "back-up"
| "back-down"
| "back-left"
| "back-right"
| "elastic"
| "elastic-up"
| "elastic-down";
type TEasing =
| "linear"
| "ease"
| "ease-in"
| "ease-out"
| "ease-in-out"
| "ease-in-sine"
| "ease-out-sine"
| "ease-in-out-sine"
| "ease-in-quad"
| "ease-out-quad"
| "ease-in-out-quad"
| "ease-in-cubic"
| "ease-out-cubic"
| "ease-in-out-cubic"
| "ease-in-quart"
| "ease-out-quart"
| "ease-in-out-quart"
| "ease-in-quint"
| "ease-out-quint"
| "ease-in-out-quint"
| "ease-in-expo"
| "ease-out-expo"
| "ease-in-out-expo"
| "ease-in-circ"
| "ease-out-circ"
| "ease-in-out-circ"
| "ease-in-back"
| "ease-out-back"
| "ease-in-out-back";
type TSectionProps = {
className?: ComponentProps<typeof motion.div>["className"];
variant?: TVariant;
easing?: TEasing;
duration?: Transition["duration"];
delay?: Transition["delay"];
children: ReactNode;
};
const variants: Record<TVariant, Variants> = {
fade: {
hidden: { opacity: 0 },
visible: { opacity: 1 },
},
none: {
hidden: {},
visible: {},
},
"slide-up": {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
},
"slide-down": {
hidden: { opacity: 0, y: -20 },
visible: { opacity: 1, y: 0 },
},
"slide-left": {
hidden: { opacity: 0, x: 20 },
visible: { opacity: 1, x: 0 },
},
"slide-right": {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 },
},
blur: {
hidden: { opacity: 0, filter: "blur(4px)" },
visible: { opacity: 1, filter: "blur(0px)" },
},
"blur-up": {
hidden: { opacity: 0, filter: "blur(4px)", y: 20 },
visible: { opacity: 1, filter: "blur(0px)", y: 0 },
},
"blur-down": {
hidden: { opacity: 0, filter: "blur(4px)", y: -20 },
visible: { opacity: 1, filter: "blur(0px)", y: 0 },
},
"blur-left": {
hidden: { opacity: 0, filter: "blur(4px)", x: 20 },
visible: { opacity: 1, filter: "blur(0px)", x: 0 },
},
"blur-right": {
hidden: { opacity: 0, filter: "blur(4px)", x: -20 },
visible: { opacity: 1, filter: "blur(0px)", x: 0 },
},
scale: {
hidden: { opacity: 0, scale: 0.95 },
visible: { opacity: 1, scale: 1 },
},
"scale-up": {
hidden: { opacity: 0, scale: 0.95, y: 10 },
visible: { opacity: 1, scale: 1, y: 0 },
},
"scale-down": {
hidden: { opacity: 0, scale: 0.95, y: -10 },
visible: { opacity: 1, scale: 1, y: 0 },
},
"zoom-in": {
hidden: { opacity: 0, scale: 0.8 },
visible: { opacity: 1, scale: 1 },
},
"zoom-out": {
hidden: { opacity: 0, scale: 1.2 },
visible: { opacity: 1, scale: 1 },
},
"rotate-in": {
hidden: { opacity: 0, rotate: -10 },
visible: { opacity: 1, rotate: 0 },
},
"rotate-in-left": {
hidden: { opacity: 0, rotate: -10, x: -20 },
visible: { opacity: 1, rotate: 0, x: 0 },
},
"rotate-in-right": {
hidden: { opacity: 0, rotate: 10, x: 20 },
visible: { opacity: 1, rotate: 0, x: 0 },
},
bounce: {
hidden: { opacity: 0, y: -20 },
visible: {
opacity: 1,
y: 0,
transition: {
type: "spring",
stiffness: 400,
damping: 10,
},
},
},
"bounce-up": {
hidden: { opacity: 0, y: 40 },
visible: {
opacity: 1,
y: 0,
transition: {
type: "spring",
stiffness: 400,
damping: 10,
},
},
},
"bounce-down": {
hidden: { opacity: 0, y: -40 },
visible: {
opacity: 1,
y: 0,
transition: {
type: "spring",
stiffness: 400,
damping: 10,
},
},
},
"bounce-left": {
hidden: { opacity: 0, x: 40 },
visible: {
opacity: 1,
x: 0,
transition: {
type: "spring",
stiffness: 400,
damping: 10,
},
},
},
"bounce-right": {
hidden: { opacity: 0, x: -40 },
visible: {
opacity: 1,
x: 0,
transition: {
type: "spring",
stiffness: 400,
damping: 10,
},
},
},
"flip-x": {
hidden: { opacity: 0, rotateX: -90 },
visible: { opacity: 1, rotateX: 0 },
},
"flip-y": {
hidden: { opacity: 0, rotateY: -90 },
visible: { opacity: 1, rotateY: 0 },
},
"roll-left": {
hidden: { opacity: 0, x: -100, rotate: -180 },
visible: { opacity: 1, x: 0, rotate: 0 },
},
"roll-right": {
hidden: { opacity: 0, x: 100, rotate: 180 },
visible: { opacity: 1, x: 0, rotate: 0 },
},
"back-up": {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
},
"back-down": {
hidden: { opacity: 0, y: -20 },
visible: { opacity: 1, y: 0 },
},
"back-left": {
hidden: { opacity: 0, x: 20 },
visible: { opacity: 1, x: 0 },
},
"back-right": {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 },
},
elastic: {
hidden: { opacity: 0, scale: 0 },
visible: {
opacity: 1,
scale: 1,
transition: {
type: "spring",
stiffness: 200,
damping: 8,
},
},
},
"elastic-up": {
hidden: { opacity: 0, y: 40, scale: 0.8 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 200,
damping: 8,
},
},
},
"elastic-down": {
hidden: { opacity: 0, y: -40, scale: 0.8 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 200,
damping: 8,
},
},
},
};
const easings: Record<TEasing, readonly [number, number, number, number]> = {
linear: [0, 0, 1, 1],
ease: [0.25, 0.1, 0.25, 1],
"ease-in": [0.42, 0, 1, 1],
"ease-out": [0, 0, 0.58, 1],
"ease-in-out": [0.42, 0, 0.58, 1],
"ease-in-sine": [0.12, 0, 0.39, 0],
"ease-out-sine": [0.61, 1, 0.88, 1],
"ease-in-out-sine": [0.37, 0, 0.63, 1],
"ease-in-quad": [0.11, 0, 0.5, 0],
"ease-out-quad": [0.5, 1, 0.89, 1],
"ease-in-out-quad": [0.45, 0, 0.55, 1],
"ease-in-cubic": [0.32, 0, 0.67, 0],
"ease-out-cubic": [0.33, 1, 0.68, 1],
"ease-in-out-cubic": [0.65, 0, 0.35, 1],
"ease-in-quart": [0.5, 0, 0.75, 0],
"ease-out-quart": [0.25, 1, 0.5, 1],
"ease-in-out-quart": [0.76, 0, 0.24, 1],
"ease-in-quint": [0.64, 0, 0.78, 0],
"ease-out-quint": [0.22, 1, 0.36, 1],
"ease-in-out-quint": [0.83, 0, 0.17, 1],
"ease-in-expo": [0.7, 0, 0.84, 0],
"ease-out-expo": [0.16, 1, 0.3, 1],
"ease-in-out-expo": [0.87, 0, 0.13, 1],
"ease-in-circ": [0.55, 0, 1, 0.45],
"ease-out-circ": [0, 0.55, 0.45, 1],
"ease-in-out-circ": [0.85, 0, 0.15, 1],
"ease-in-back": [0.36, 0, 0.66, -0.56],
"ease-out-back": [0.34, 1.56, 0.64, 1],
"ease-in-out-back": [0.68, -0.6, 0.32, 1.6],
};
export const Section = ({
className,
variant = "blur",
easing = "ease-out-cubic",
duration = 0.3,
delay = 0,
children,
}: TSectionProps) => {
const shouldReduceMotion = useReducedMotion();
if (shouldReduceMotion) {
return <div className={cn("grid gap-4", className)}>{children}</div>;
}
return (
<motion.div
className={cn("grid gap-4", className)}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "0px 0px -100px 0px" }}
variants={variants[variant]}
transition={{
duration,
delay,
ease: easings[easing],
}}
>
{children}
</motion.div>
);
};
Updated: 11/1/2025