Rolling Word
Installation
npx shadcn@latest add https://uibrik.com/r/rolling-word.jsonInstall the packages
npm install motion react-use-measureAdd the cn helper
Imports
cnfrom@/lib/utils, whichshadcn initcreates. Add it by hand without shadcn.Copy the source into your project
components/uibrik/rolling-word/rolling-word.tsx"use client"; import { cn } from "@/lib/utils"; import { AnimatePresence, motion } from "motion/react"; import { useMemo } from "react"; import useMeasure from "react-use-measure"; import { EASE_OUT, TEXT, pick, type EffectProps } from "../_text-effects/shared"; // 07 — Each press rolls in the next word on a drum. Letters sit on a // cylinder (their transform origin is pushed back behind the glyph), so the // old word turns up and away while the new one turns in from below, letter by // letter. A vertical mask softens both edges instead of blurring, and the box // resizes to the incoming word so the words before it never jump. const ROTATOR_WORDS = ["calm", "honest", "lasting", "yours"]; const DRUM_EASE = [0.65, 0, 0.35, 1] as const; const drumWordFor = (stagger: number) => ({ enter: {}, center: { transition: { staggerChildren: stagger } }, exit: { transition: { staggerChildren: stagger } }, }); const drumLetterFor = (duration: number, exitDuration: number) => ({ enter: { rotateX: -90, opacity: 0 }, center: { rotateX: 0, opacity: 1, transition: { duration, ease: DRUM_EASE } }, exit: { rotateX: 90, opacity: 0, transition: { duration: exitDuration, ease: DRUM_EASE } }, }); export function RollingWord({ play, words = ROTATOR_WORDS, prefix = "Build something", duration = 0.45, exitDuration = 0.35, stagger = 0.03, className, }: EffectProps & { className?: string; words?: string[]; /** Fixed text before the rolling word. */ prefix?: string; /** Seconds for a letter to roll in. */ duration?: number; /** Seconds for a letter to roll out. */ exitDuration?: number; /** Seconds between letters. */ stagger?: number; }) { const word = pick(words, play); const drumWord = useMemo(() => drumWordFor(stagger), [stagger]); const drumLetter = useMemo(() => drumLetterFor(duration, exitDuration), [duration, exitDuration]); const [ref, { width }] = useMeasure(); return ( <p className={cn(TEXT, "flex items-baseline", className)} aria-label={`${prefix} ${word}`}> <span aria-hidden>{prefix} </span> <motion.span aria-hidden className="relative inline-flex" animate={{ width: width || "auto" }} transition={{ duration: 0.4, ease: EASE_OUT }} > <span ref={ref} className="invisible whitespace-nowrap"> {word} </span> {/* A mask clips everything outside its own box, so it lives on a layer that reaches past the right edge: an outgoing word wider than the shrinking box keeps its last letters until it has rolled away. */} <span className="pointer-events-none absolute -inset-y-[0.6em] left-0 -right-[3.2em] perspective-[400px] [mask-image:linear-gradient(transparent,black_30%,black_70%,transparent)]"> <AnimatePresence initial={false}> <motion.span key={play} className="absolute left-0 top-[0.6em] flex whitespace-nowrap text-zinc-400 transform-3d dark:text-zinc-500" variants={drumWord} initial="enter" animate="center" exit="exit" > {Array.from(word).map((ch, i) => ( <motion.span key={i} className="inline-block backface-hidden" style={{ transformOrigin: "50% 50% -0.6em" }} variants={drumLetter} > {ch} </motion.span> ))} </motion.span> </AnimatePresence> </span> </motion.span> </p> ); }
Adds the files to
components/uibrik/rolling-word and installs motion and react-use-measure. With the @uibrik namespace set up: npx shadcn@latest add @uibrik/rolling-word.Props
RollingWord- prefix
- Type string
- "Build something"
- Fixed text before the rolling word.
- words
- Type string[]
- ["calm","honest","lasting","yours"]
- Cycled through, one per play.
- duration
- Type number
- 0.45
- Time for a letter to roll in.
- exitDuration
- Type number
- 0.35
- Time for a letter to roll out.
- stagger
- Type number
- 0.03
- Delay between letters.
- playrequired
- Type number
- –
- Each change plays the effect again; 0 shows its resting state.
- className
- Type string
- –
- Classes for the line, merged over the defaults.