/
  1. Catalog
  2. Text Reveal Box

component

Text Reveal Box

Scroll-pinned word-by-word text reveal with a highlight flash, powered by GSAP ScrollTrigger.

Released September 14, 2026
Made by Axyl

Installation

Install the following dependencies:

Install the following registry dependencies:

Copy and paste the following code into your project:

components/sora-ui/catalog/text-reveal-box.tsx
Loading code…

Update the import paths to match your project setup.

File Structure

text-reveal-box.tsx
scroll-trigger-utils.tsx

Usage

Basic

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}
    />
  );
}

Dark serif preset (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.

Custom class map (advanced)

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}
/>;

Docs preview

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.

Keywords & timing

<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 }}
/>

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 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.

Class slots

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 wrapper
  • container — width container (default max-w-3xl)
  • paragraph — each <p>
  • word — .trb-word wrapper
  • keywordWrapper / keyword — keyword pill (before: + --kw-color)

CSS variables

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–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

PropTypeDefault
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
-

Accessibility

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.

Loading preview...