Getting Started
Components
Search for a command to run...
import { TextRevealScroll } from "@/registry/tween/text-reveal-scroll"
// Reveal on scroll, but re-trigger every time a block re-enters (once={false})
// and use a forgiving start so blocks still reveal when a smooth-scroll or
// instant jump skips past them — keeps every section legible at any size.
const revealProps = { once: false, start: "top 90%" } as const
export function TextRevealScrollDemo() {
return (
<div className="relative w-full bg-white [font-family:ui-sans-serif,system-ui,sans-serif] text-black">
{/* Hero — dark scrim + bright text so the heading stays legible over
the photo at any viewport size; centered with a readable measure. */}
<section className="relative flex h-[var(--preview-h,100vh)] w-full items-center justify-center overflow-hidden bg-neutral-900 p-8">
<img
src="https://images.unsplash.com/photo-1722110813281-594c2f6c1dbd?q=80&w=1800&h=1100&fit=crop&crop=faces,center&auto=format"
alt=""
className="absolute inset-0 z-0 h-full w-full object-cover"
/>
<div className="absolute inset-0 z-0 bg-black/55" />
<nav className="absolute top-0 left-0 z-10 flex w-full items-center px-8 py-6 text-xs font-medium tracking-wide text-white/70 uppercase">
<span className="flex-1">Tween</span>
<div className="flex flex-1 justify-center gap-6 max-md:hidden">
<span>Work</span>
<span>Studio</span>
<span>About</span>
<span>Contact</span>
</div>
<span className="flex-1 text-right">Menu</span>
</nav>
<div className="relative z-10 mx-auto max-w-3xl text-center">
<TextRevealScroll delay={0.3} {...revealProps}>
<h1 className="text-[clamp(2.25rem,5vw,4rem)] leading-[1.05] font-medium tracking-[-0.03em] text-white">
Words that arrive exactly when the moment is right.
</h1>
</TextRevealScroll>
<TextRevealScroll delay={0.5} {...revealProps}>
<p className="mx-auto mt-6 max-w-xl text-base text-white/70 sm:text-lg">
A scroll-reveal primitive that splits any text into lines and
brings them up in rhythm with the reader.
</p>
</TextRevealScroll>
</div>
</section>
{/* Statement */}
<section className="mx-auto flex min-h-[var(--preview-h,100vh)] w-full max-w-5xl flex-col justify-center gap-6 px-8 py-24">
<TextRevealScroll {...revealProps}>
<span className="block text-xs font-medium tracking-wide text-neutral-500 uppercase">
Craft & Motion Studio
</span>
</TextRevealScroll>
<TextRevealScroll {...revealProps}>
<h2 className="text-[clamp(1.75rem,4.5vw,3.25rem)] leading-[1.1] font-medium tracking-[-0.03em]">
We build interfaces that move with intention, where every transition
earns its place and nothing distracts from the story you came here
to tell.
</h2>
</TextRevealScroll>
</section>
{/* Two-column detail */}
<section className="mx-auto flex w-full max-w-5xl gap-12 px-8 py-24 max-md:flex-col max-md:gap-8">
<div className="flex-1">
<TextRevealScroll {...revealProps}>
<h2 className="text-[clamp(1.75rem,4vw,2.75rem)] leading-[1.1] font-medium tracking-[-0.03em]">
Motion
<br />
With Purpose
</h2>
</TextRevealScroll>
</div>
<div className="flex-1">
<TextRevealScroll {...revealProps}>
<div className="space-y-4 text-base leading-relaxed text-neutral-700 sm:text-lg">
<p>
Every component reveals content the way a great editor would: in
rhythm with the reader, never rushing the eye, always landing
the line that matters most at exactly the right beat.
</p>
<p>
Animations are tuned for performance first — transforms only, no
layout thrash, and full respect for reduced-motion preferences,
so it stays fast on every device.
</p>
<p>
Drop a heading, a paragraph, or an entire section inside and it
simply works. No configuration rituals, just expressive motion.
</p>
</div>
</TextRevealScroll>
</div>
</section>
{/* Closing panel */}
<section className="flex min-h-[var(--preview-h,100vh)] w-full flex-col justify-center bg-neutral-950 px-8 py-24 text-white">
<div className="mx-auto w-full max-w-5xl">
<TextRevealScroll {...revealProps}>
<span className="block text-xs font-medium tracking-wide text-white/50 uppercase">
Behind the Motion
</span>
</TextRevealScroll>
<TextRevealScroll {...revealProps}>
<h2 className="mt-6 text-[clamp(1.75rem,4.5vw,3.25rem)] leading-[1.1] font-medium tracking-[-0.03em] text-white">
Great motion is invisible until it matters. We obsess over the
easing curve, the stagger, the half-second of delay that turns a
plain reveal into something you feel.
</h2>
</TextRevealScroll>
<footer className="mt-16 flex items-end justify-between gap-4 border-t border-white/15 pt-8 text-xs font-medium tracking-wide text-white/50 uppercase">
<span>Tween & Co</span>
<span className="max-md:hidden">Tween Studio 2025</span>
</footer>
</div>
</section>
</div>
)
}
import { TextRevealScroll } from "@/components/ui/text-reveal-scroll"Wrap any heading and add a short delay before the lines reveal:
<TextRevealScroll delay={0.5}>
<h1>Design that moves with intent.</h1>
</TextRevealScroll>Pass multiple children and the component renders a wrapper div around them:
<TextRevealScroll>
<h1>Crafted</h1>
<h1>Animated</h1>
<h1>Shipped</h1>
</TextRevealScroll>Disable the scroll trigger to play the reveal immediately on mount:
<TextRevealScroll animateOnScroll={false} delay={0.2}>
<h1>Built for motion.</h1>
</TextRevealScroll>| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | One or more elements whose text should reveal. |
animateOnScroll | boolean | true | When false, the reveal plays immediately on mount. |
delay | number | 0 | Delay before the first line animates, in seconds. |
duration | number | 1 | Tween duration per line. |
stagger | number | 0.1 | Stagger between consecutive lines. |
ease | string | "power4.out" | GSAP ease. |
start | string | "top 75%" | ScrollTrigger start position. |
once | boolean | true | When true, the animation runs once and the trigger self-cleans. |
className | string | — | Extra classes appended to the wrapped element (or wrapper div). |
SplitText runs in lines mode with mask: "lines" so each line lives inside its own clipped wrapper. The lines start at y: 100% and tween back to 0% with a stagger, so each one slides up from behind its own mask edge.
If your text uses text-indent, the component reads the computed indent and applies it as padding-left on the first split line, so the original layout is preserved after the split.
When the wrapped element unmounts, the component reverts the SplitText and kills any associated ScrollTrigger, leaving the DOM and GSAP state clean.