Falling Letters
Installation
npx shadcn@latest add https://uibrik.com/r/falling-letters.jsonInstall the packages
npm install motionAdd the cn helper
Imports
cnfrom@/lib/utils, whichshadcn initcreates. Add it by hand without shadcn.Copy the source into your project
components/uibrik/falling-letters/falling-letters.tsx"use client"; import { cn } from "@/lib/utils"; import { motion, type Easing } from "motion/react"; import { useLayoutEffect, useRef, useState } from "react"; import { TEXT, glyph, type EffectProps } from "../_text-effects/shared"; // 15 — Letters fall and land with weight. Each one accelerates down while // stretched and slightly tilted, squashes against the baseline on impact // (scaled from its foot), gives a small rebound and straightens. Start times // follow each letter's position on the line, so wide glyphs hold the wave // back, plus a small fixed jitter so the wave never feels mechanical. const FALL_TIMES = [0, 0.55, 0.75, 1]; const FALL_EASE: Easing[] = ["easeIn", "easeOut", "easeInOut"]; // Deterministic per-letter variation in [0, 1): the same letter always // falls the same way, and rendering stays pure. const jitter = (i: number) => ((i * 37 + 11) % 17) / 17; export function FallingLetters({ play, text = "Falling into place", duration = 0.6, spread = 0.35, drop = 36, }: EffectProps & { text?: string; /** Seconds for one letter to fall, land and settle. */ duration?: number; /** Seconds the wave takes to cross the line. */ spread?: number; /** Px each letter falls from. */ drop?: number; }) { const lettersRef = useRef<(HTMLSpanElement | null)[]>([]); const [delays, setDelays] = useState<number[]>([]); useLayoutEffect(() => { const letters = lettersRef.current; const last = letters[letters.length - 1]; const total = last ? last.offsetLeft + last.offsetWidth : 1; setDelays(letters.map((el, i) => ((el?.offsetLeft ?? 0) / total) * spread + jitter(i) * 0.08)); }, [text, spread]); return ( <p aria-label={text} className={cn(TEXT, "flex")}> {Array.from(text).map((ch, i) => { const tilt = (jitter(i + 5) - 0.5) * 16; return ( <motion.span key={`${play}-${i}`} aria-hidden ref={(el) => { lettersRef.current[i] = el; }} className="inline-block origin-bottom" initial={play ? { y: -drop, opacity: 0, rotate: tilt, scaleX: 0.92, scaleY: 1.12 } : false} animate={ play ? { y: [-drop, 0, -3, 0], opacity: [0, 1, 1, 1], rotate: [tilt, 0, 0, 0], scaleX: [0.92, 1.14, 0.98, 1], scaleY: [1.12, 0.82, 1.03, 1], } : undefined } transition={{ duration, delay: delays[i] ?? 0, times: FALL_TIMES, ease: FALL_EASE }} > {glyph(ch)} </motion.span> ); })} </p> ); }
Adds the files to
components/uibrik/falling-letters and installs motion. With the @uibrik namespace set up: npx shadcn@latest add @uibrik/falling-letters.Props
FallingLetters- text
- Type string
- "Falling into place"
- The copy to animate.
- duration
- Type number
- 0.6
- Time for one letter to fall and settle.
- spread
- Type number
- 0.35
- Time for the wave to cross the line.
- drop
- Type number
- 36
- Height each letter falls from.
- playrequired
- Type number
- –
- Each change plays the effect again; 0 shows its resting state.