component
Scroll-pinned word-by-word text reveal with a highlight flash, powered by GSAP ScrollTrigger.
Dependencies
Installation
Client component — lazy-loads GSAP ScrollTrigger in useEffect (SSR-safe). Scroll is a tall track + sticky viewport (not GSAP pin): pinDuration sets how many viewport-heights you scroll while words reveal. Respects prefers-reduced-motion.
How to use
Basic
Neutral defaults (bg-background, text-foreground). Style via *ClassName props.
import { TextRevealBox } from "@/components/sora-ui/texts/text-reveal-box";
const paragraphs = [
"Your first paragraph of manifesto copy.",
"A second paragraph that continues the scroll-driven reveal.",
];
export default function Section() {
return (
<TextRevealBox
paragraphs={paragraphs}
paragraphClassName="text-center text-3xl font-medium tracking-tight md:text-5xl"
highlightBg="60, 60, 60"
pinDuration={4}
/>
);
}Studio preset
Bundled dark manifesto typography — pass TEXT_REVEAL_BOX_STUDIO_CLASSES into class slots:
import {
TEXT_REVEAL_BOX_STUDIO_CLASSES as studio,
TextRevealBox,
} from "@/components/sora-ui/texts/text-reveal-box";
<TextRevealBox
className={studio.root}
stickyClassName={studio.sticky}
containerClassName={studio.container}
paragraphClassName={studio.paragraph}
wordClassName={studio.word}
keywordClassName={studio.keyword}
paragraphs={paragraphs}
highlightBg="60, 60, 60"
pinDuration={4}
/>;Docs preview
embedded = transparent surface + theme-aware highlight. Pass scroller when inside a scroll panel.
<TextRevealBox embedded scroller={viewport} paragraphs={paragraphs} pinDuration={4} />Keywords & timing
<TextRevealBox
paragraphs={["Systems design meets psychological tension."]}
keywords={["systems", "tension"]}
keywordColors={{ systems: "#c8e600", tension: "#f7f5f0" }}
keywordClassName={studio.keyword}
highlightBg="60, 60, 60"
pinDuration={4}
/>Default matcher: lowercaseNormalizeWord (strip edge punctuation, lowercase). Override with normalizeWord or matchKeyword.
Lock words after reveal — skip the reverse fade:
<TextRevealBox
paragraphs={paragraphs}
timing={{ revealPortion: 0.7, revealOverlap: 15, reverseOnScroll: false }}
/>Styling
Word spacing
Words are adjacent <span> nodes without spaces in markup. Gaps come from built-in mr-[0.2rem] mb-[0.2rem] on .trb-word. If you override wordClassName, keep equivalent margins or pills will touch.
GSAP inline styles
ScrollTrigger sets opacity and backgroundColor on each .trb-word, and opacity on the inner span. CSS alone cannot drive the reveal — only initial hidden state and pill shape.
Class slots
Pass any of these to override a layer (cn() merges after built-ins):
className— root<section data-slot="text-reveal-box">trackClassName— scroll track (ScrollTrigger trigger)stickyClassName— sticky viewport (h-svh,p-8)innerClassName— flex center wrappercontainerClassName— width container (defaultmax-w-3xl)paragraphClassName— each<p>wordClassName—.trb-wordwrapperkeywordWrapperClassName/keywordClassName— keyword pill (before:+--kw-color)
CSS variables
On the root section:
--trb-pin-duration— frompinDuration; drivesh-[calc(var(--trb-pin-duration)*100svh)]--trb-highlight-bg— RGB triplet, norgb()wrapper (e.g.60,60,60)--trb-highlight-alpha— flash opacity0–1
On keyword inner spans: --kw-color from keywordColors.
Highlight resolves at init: CSS vars on root (e.g. embedded) → else highlightBg + highlightAlpha.
<TextRevealBox
className="[--trb-highlight-bg:60,60,60] [--trb-highlight-alpha:0.85]"
paragraphs={paragraphs}
/>Built-in structure (reference)
Track — h-[calc(var(--trb-pin-duration)*100svh)], max-lg:…100dvh, @/preview:…100cqh for catalog preview.
Word wrapper — inline-block rounded-lg p-[0.1rem_0.2rem] opacity-0 will-change-[background-color,opacity] plus spacing margins above.
Scroll phases — first revealPortion (default 70%) reveals with overlap; remainder reverses highlight when reverseOnScroll is true.
Props
| Prop | Type | Default |
|---|---|---|
paragraphs? | string[] | [] |
pinDuration? | number | 4 |
scroller? | Element | Window | - |
highlightBg? | string | "237, 235, 231" |
highlightAlpha? | number | 1 |
timing? | TextRevealBoxTiming | - |
embedded? | boolean | false |
keywords? | string[] | [] |
keywordColors? | Record<string, string> | {} |
normalizeWord? | (word: string) => string | - |
matchKeyword? | (word: string, keywords: string[]) => boolean | - |
className? | string | - |
trackClassName? | string | - |
stickyClassName? | string | - |
innerClassName? | string | - |
containerClassName? | string | - |
paragraphClassName? | string | - |
wordClassName? | string | - |
keywordWrapperClassName? | string | - |
keywordClassName? | string | - |