A text roll component that rotates each character, fully customizable for nice text animations.
1'use client';
2import {
3 motion,
4 VariantLabels,
5 Target,
6 TargetAndTransition,
7 Transition,
8} from 'motion/react';
9
10export type TextRollProps = {
11 children: string;
12 duration?: number;
13 getEnterDelay?: (index: number) => number;
14 getExitDelay?: (index: number) => number;
15 className?: string;
16 transition?: Transition;
17 variants?: {
18 enter: {
19 initial: Target | VariantLabels | boolean;
20 animate: TargetAndTransition | VariantLabels;
21 };
22 exit: {
23 initial: Target | VariantLabels | boolean;
24 animate: TargetAndTransition | VariantLabels;
25 };
26 };
27 onAnimationComplete?: () => void;
28};
29
30export function TextRoll({
31 children,
32 duration = 0.5,
33 getEnterDelay = (i) => i * 0.1,
34 getExitDelay = (i) => i * 0.1 + 0.2,
35 className,
36 transition = { ease: 'easeIn' },
37 variants,
38 onAnimationComplete,
39}: TextRollProps) {
40 const defaultVariants = {
41 enter: {
42 initial: { rotateX: 0 },
43 animate: { rotateX: 90 },
44 },
45 exit: {
46 initial: { rotateX: 90 },
47 animate: { rotateX: 0 },
48 },
49 } as const;
50
51 const letters = children.split('');
52
53 return (
54 <span className={className}>
55 {letters.map((letter, i) => {
56 return (
57 <span
58 key={i}
59 className='relative inline-block [perspective:10000px] [transform-style:preserve-3d] [width:auto]'
60 aria-hidden='true'
61 >
62 <motion.span
63 className='absolute inline-block [backface-visibility:hidden] [transform-origin:50%_25%]'
64 initial={
65 variants?.enter?.initial ?? defaultVariants.enter.initial
66 }
67 animate={
68 variants?.enter?.animate ?? defaultVariants.enter.animate
69 }
70 transition={{
71 ...transition,
72 duration,
73 delay: getEnterDelay(i),
74 }}
75 >
76 {letter === ' ' ? '\u00A0' : letter}
77 </motion.span>
78 <motion.span
79 className='absolute inline-block [backface-visibility:hidden] [transform-origin:50%_100%]'
80 initial={variants?.exit?.initial ?? defaultVariants.exit.initial}
81 animate={variants?.exit?.animate ?? defaultVariants.exit.animate}
82 transition={{
83 ...transition,
84 duration,
85 delay: getExitDelay(i),
86 }}
87 onAnimationComplete={
88 letters.length === i + 1 ? onAnimationComplete : undefined
89 }
90 >
91 {letter === ' ' ? '\u00A0' : letter}
92 </motion.span>
93 <span className='invisible'>
94 {letter === ' ' ? '\u00A0' : letter}
95 </span>
96 </span>
97 );
98 })}
99 <span className='sr-only'>{children}</span>
100 </span>
101 );
102}
103