motion-primitives
components
ui
animation
motion

text-roll

A text roll component that rotates each character, fully customizable for nice text animations.

3d
animated
form
positioning
special
text
transform
transition
View Docs

Source Code

Files
text-roll.tsx
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