Command Palette

Search for a command to run...

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/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>
    );
}

Update the import paths to match your project setup.