component
Scroll-pinned word-by-word text reveal with a highlight flash, powered by GSAP ScrollTrigger.
Loading code…Neutral defaults (bg-background, text-foreground). Style via the classNames prop.
import { TextRevealBox } from "@/components/sora-ui/catalog/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}
classNames={{
paragraph: "text-center text-3xl font-medium tracking-tight md:text-5xl",
}}
highlightBg="60, 60, 60"
pinDuration={4}
/>
);
}variant="studio")Near-black canvas, uppercase serif copy, and keyword pills tuned for long-form manifesto sections:
<TextRevealBox
paragraphs={paragraphs}
highlightBg="60, 60, 60"
pinDuration={4}
variant="studio"
/>variant="minimal" (default) uses theme tokens (bg-background, text-foreground). Override any slot via the classNames prop.
Import TEXT_REVEAL_BOX_STUDIO_CLASSES to extend the studio preset:
import {
TEXT_REVEAL_BOX_STUDIO_CLASSES as studio,
TextRevealBox,
} from "@/components/sora-ui/catalog/text-reveal-box";
<TextRevealBox
variant="studio"
classNames={{ paragraph: cn(studio.paragraph, "text-balance") }}
paragraphs={paragraphs}
pinDuration={4}
/>;embedded = transparent surface + theme-aware highlight (minimal) or studio canvas (variant="studio"). Pass scroller when inside a scroll panel.
<TextRevealBox
embedded
scroller={viewport}
paragraphs={paragraphs}
pinDuration={4}
variant="studio"
/>Scroll uses a tall track + sticky viewport (not GSAP pin): pinDuration sets how many viewport-heights you scroll while words reveal.
<TextRevealBox
paragraphs={["Systems design meets psychological tension."]}
keywords={["systems", "tension"]}
keywordColors={{ systems: "#c8e600", tension: "#f7f5f0" }}
classNames={{ keyword: 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 }}
/>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 classNames.word, 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.
className styles the root <section data-slot="text-reveal-box">. Pass any of
these to classNames to override an inner layer (cn() merges after built-ins):
track — scroll track (ScrollTrigger trigger)sticky — sticky viewport (h-svh, p-8)inner — flex center wrappercontainer — width container (default max-w-3xl)paragraph — each <p>word — .trb-word wrapperkeywordWrapper / keyword — keyword pill (before: + --kw-color)On the root section:
--trb-pin-duration — from pinDuration; drives h-[calc(var(--trb-pin-duration)*100svh)]--trb-highlight-bg — RGB triplet, no rgb() wrapper (e.g. 60,60,60)--trb-highlight-alpha — flash opacity 0–1On 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}
/>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.
| Prop | Type | Default |
|---|---|---|
paragraphs? | string[] | [] |
pinDuration? | number | 4 |
refreshPriority? | number | -1 |
scroller? | Element | Window | - |
highlightBg? | string | "237, 235, 231" |
highlightAlpha? | number | 1 |
timing? | TextRevealBoxTiming | - |
embedded? | boolean | false |
variant? | "minimal" | "studio" | minimal |
keywords? | string[] | [] |
keywordColors? | Record<string, string> | {} |
normalizeWord? | (word: string) => string | - |
matchKeyword? | (word: string, keywords: string[]) => boolean | - |
className? | string | - |
classNames? | TextRevealBoxClassNames | - |
All text remains semantic and readable in the DOM throughout the scroll sequence. Screen readers and search engine crawlers access the underlying paragraphs without being affected by the visual highlight animation.
Built by Axyl. A motion-first component registry for React.