motion-primitives
Installation
Install the following dependencies:
pnpm add motion
Create a file at components/motion/text-roll.tsx
and paste the given code.
"use client";
import {
motion,
VariantLabels,
Target,
TargetAndTransition,
Transition,
} from "motion/react";
export type TextRollProps = {
children: string;
duration?: number;
getEnterDelay?: (index: number) => number;
getExitDelay?: (index: number) => number;
className?: string;
transition?: Transition;
variants?: {
enter: {
initial: Target | VariantLabels | boolean;
animate: TargetAndTransition | VariantLabels;
};
exit: {
initial: Target | VariantLabels | boolean;
animate: TargetAndTransition | VariantLabels;
};
};
onAnimationComplete?: () => void;
};
export function TextRoll({
children,
duration = 0.5,
getEnterDelay = (i) => i * 0.1,
getExitDelay = (i) => i * 0.1 + 0.2,
className,
transition = { ease: "easeIn" },
variants,
onAnimationComplete,
}: TextRollProps) {
const defaultVariants = {
enter: {
initial: { rotateX: 0 },
animate: { rotateX: 90 },
},
exit: {
initial: { rotateX: 90 },
animate: { rotateX: 0 },
},
} as const;
const letters = children.split("");
return (
<span className={className}>
{letters.map((letter, i) => {
return (
<span
key={i}
className="relative inline-block [perspective:10000px] [transform-style:preserve-3d] [width:auto]"
aria-hidden="true"
>
<motion.span
className="absolute inline-block [backface-visibility:hidden] [transform-origin:50%_25%]"
initial={
variants?.enter?.initial ??
defaultVariants.enter.initial
}
animate={
variants?.enter?.animate ??
defaultVariants.enter.animate
}
transition={{
...transition,
duration,
delay: getEnterDelay(i),
}}
>
{letter === " " ? "\u00A0" : letter}
</motion.span>
<motion.span
className="absolute inline-block [backface-visibility:hidden] [transform-origin:50%_100%]"
initial={
variants?.exit?.initial ??
defaultVariants.exit.initial
}
animate={
variants?.exit?.animate ??
defaultVariants.exit.animate
}
transition={{
...transition,
duration,
delay: getExitDelay(i),
}}
onAnimationComplete={
letters.length === i + 1
? onAnimationComplete
: undefined
}
>
{letter === " " ? "\u00A0" : letter}
</motion.span>
<span className="invisible">
{letter === " " ? "\u00A0" : letter}
</span>
</span>
);
})}
<span className="sr-only">{children}</span>
</span>
);
}