Text Roll

An interactive collection of stacked headings that reveal individual content sections with a click.

motion-primitives

Installation

Install the following dependencies:

pnpm add motion

Create a file at components/motions/text-roll.tsx and paste the given code.

"use client";
import { motion, Transition } from "motion/react";
 
export type TextRollProps = {
    children: string;
    duration?: number;
    getEnterDelay?: (index: number) => number;
    getExitDelay?: (index: number) => number;
    className?: string;
    transition?: Transition;
    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" },
    onAnimationComplete,
}: TextRollProps) {
    const letters = children.split("");
 
    return (
        <span className={className}>
            {letters.map((letter, i) => (
                <span
                    key={i}
                    className="relative inline-block [width:auto] [perspective:10000px] [transform-style:preserve-3d]"
                    aria-hidden="true"
                >
                    <motion.span
                        className="absolute inline-block [transform-origin:50%_25%] [backface-visibility:hidden]"
                        initial={{ rotateX: 0 }}
                        animate={{ rotateX: 90 }}
                        transition={{
                            ...transition,
                            duration,
                            delay: getEnterDelay(i),
                        }}
                    >
                        {letter === " " ? "\u00A0" : letter}
                    </motion.span>
                    <motion.span
                        className="absolute inline-block [transform-origin:50%_100%] [backface-visibility:hidden]"
                        initial={{ rotateX: 90 }}
                        animate={{ rotateX: 0 }}
                        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>
    );
}

Update the import paths to match your project setup.