Stagger Text
loomui/stagger-textFreeComponent
Words or characters that rise into place one after another, on mount or on scroll.
Live preview
live · rendered by the registry
Rendered by loomui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://loomui.design/r/stagger-text.jsonSource
1"use client"23import * as React from "react"45import { cn } from "@/lib/utils"67export interface StaggerTextProps extends Omit<8 React.ComponentProps<"span">,9 "children"10> {11 /** The text to reveal. Only plain text, so the split stays predictable. */12 children: string13 /** Split the text into words or into single characters. */14 by?: "word" | "character"15 /** Seconds between one unit starting and the next. */16 stagger?: number17 /** Seconds for a single unit to finish arriving. */18 duration?: number19 /** Seconds to wait before the first unit starts. */20 delay?: number21 /** Hold until the text scrolls into view instead of running on mount. */22 startOnView?: boolean23 /** Replay every time the text re-enters the viewport. */24 repeat?: boolean25 /** Render the finished state with no animation. */26 disabled?: boolean27}2829/** Splits on whitespace but keeps the separators so spacing survives. */30function split(text: string, by: "word" | "character") {31 if (by === "character") {32 return Array.from(text)33 }34 return text.split(/(\s+)/)35}3637export function StaggerText({38 children,39 className,40 by = "word",41 stagger = 0.04,42 duration = 0.5,43 delay = 0,44 startOnView = false,45 repeat = false,46 disabled = false,47 ...props48}: StaggerTextProps) {49 const ref = React.useRef<HTMLSpanElement>(null)50 const [active, setActive] = React.useState(!startOnView)5152 React.useEffect(() => {53 if (!startOnView || disabled) {54 return55 }5657 const node = ref.current58 if (!node || typeof IntersectionObserver === "undefined") {59 setActive(true)60 return61 }6263 const observer = new IntersectionObserver(64 ([entry]) => {65 if (entry.isIntersecting) {66 setActive(true)67 if (!repeat) {68 observer.disconnect()69 }70 } else if (repeat) {71 setActive(false)72 }73 },74 { threshold: 0.2 }75 )7677 observer.observe(node)78 return () => observer.disconnect()79 }, [startOnView, repeat, disabled])8081 const units = React.useMemo(() => split(children, by), [children, by])8283 return (84 <span85 ref={ref}86 data-slot="stagger-text"87 className={cn("inline-block", className)}88 {...props}89 >90 {/* The whole string is announced once; the pieces are decoration. */}91 <span className="sr-only">{children}</span>92 <span aria-hidden="true">93 {units.map((unit, index) => {94 if (/^\s+$/.test(unit)) {95// … truncated
What it pulls in
Other registry items
Files it writes
More from loomui
All 91 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Aurora Backdropaurora-backdrop | loomui | Components | Free | no deps | |
| Bento Gridbento-grid | loomui | Components | Free | no deps | |
| Card Stackcard-stack | loomui | Components | Free | no deps | |
| Compare Slidercompare-slider | loomui | Components | Free | no deps | |
| Confetti Buttonconfetti-button | loomui | Components | Free | no deps | |
| Count Upcount-up | loomui | Components | Free | no deps | |
| Credit Cardcredit-card | loomui | Components | Free | no deps | |
| Drawerdrawer | loomui | Components | Free | 1 dep |
