Line Reveal
Headings and paragraphs that rise line by line out of a mask.
Customize
<LineReveal children={children} />Changes show live and stay in the address, so a link keeps them.
Code
"use client";
import { createElement, useRef } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import { useGSAP } from "@gsap/react";
// A heading or paragraph that rises line by line out of a mask when it scrolls into view.
// Optionally the words start faint and fill in as you scroll, like reading along with your finger.
// The text stays real text: selectable, and read out as one sentence by screen readers.
if (typeof window !== "undefined") gsap.registerPlugin(useGSAP, ScrollTrigger, SplitText);
export type LineRevealProps = {
/** The text to show. Plain text only (the lines are cut from it). */
children: string;
/** The tag to render: a page title, a section title or a paragraph. */
as?: "h1" | "h2" | "p";
/** Delay between one line starting and the next, in seconds. */
stagger?: number;
/** How long each line takes to rise, in seconds. */
duration?: number;
/** Fill the words in one by one as you scroll through the text. */
highlight?: boolean;
/** The colour a word turns when it is filled in. Empty keeps the text colour. */
highlightColor?: string;
/** When the reveal starts: "<element edge> <viewport edge>", e.g. "top 85%". */
start?: string;
/** The scrolling element. Leave it out to use the page; give it a selector or an element for an inner scroller. */
scroller?: string | HTMLElement;
/** Extra classes: put the font size, weight and colour here. */
className?: string;
};
export function LineReveal({
children,
as = "p",
stagger = 0.08,
duration = 1,
highlight = false,
highlightColor = "#ff5a36",
start = "top 85%",
scroller,
className = "",
}: LineRevealProps) {
const textRef = useRef<HTMLElement>(null);
useGSAP(
() => {
const element = textRef.current;
if (!element) return;
// The text is hidden until it is split, so you never see it jump. Show it again either way.
element.style.visibility = "visible";
// Reduced motion: leave the text exactly as written, fully readable
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
let fill: gsap.core.Tween | undefined;
const split = SplitText.create(element, {
type: highlight ? "lines,words" : "lines",
mask: "lines",
linesClass: "line-reveal-line",
// Re-cut the lines when the width changes or the web font arrives
autoSplit: true,
onSplit(self) {
// The mask clips to the line box, which cuts the tails of g, y and p. Give it a little room.
self.masks.forEach((mask) => {
const box = mask as HTMLElement;
box.style.paddingBottom = "0.14em";
box.style.marginBottom = "-0.14em";
});
// This runs again after every re-split, so drop the old fill first
fill?.scrollTrigger?.kill();
fill?.kill();
if (highlight) {
const color = highlightColor || getComputedStyle(element).color;
// Words start faint and fill in as the scrollbar moves (scrub), one after another
fill = gsap.fromTo(
self.words,
{ opacity: 0.18 },
{
opacity: 1,
color,
ease: "none",
stagger: 0.12,
duration: 0.4,
scrollTrigger: { trigger: element, scroller, start, end: "bottom 45%", scrub: 0.4 },
},
);
}
// Returning the tween lets SplitText keep it in step when it re-splits
return gsap.from(self.lines, {
yPercent: 115,
rotate: 3,
transformOrigin: "0% 100%",
duration,
ease: "power4.out",
stagger,
scrollTrigger: { trigger: element, scroller, start, toggleActions: "play none none none" },
});
},
});
return () => {
fill?.scrollTrigger?.kill();
fill?.kill();
split.revert();
};
},
{ scope: textRef, dependencies: [children, as, stagger, duration, highlight, highlightColor, start, scroller], revertOnUpdate: true },
);
// The key makes React build a fresh element when the text or tag changes, because SplitText
// rewrites the inside of the old one.
return createElement(as, { ref: textRef, key: `${as}:${children}`, className, style: { visibility: "hidden" } }, children);
}
The component. Installs to components/dimaac/line-reveal.tsx.
Install
With the shadcn CLI
npx shadcn@latest add https://ui.dimaac.com/r/line-reveal.jsonIt adds components/dimaac/line-reveal.tsx and installs what it imports. With the @dimaac namespace in components.json (Docs) it's just npx shadcn@latest add @dimaac/line-reveal.
By hand
npm install gsap @gsap/react- Install the packages it uses: gsap, @gsap/react.
- Copy the component file below into components/dimaac/line-reveal.tsx.
- It's styled with Tailwind v4 classes. If that folder sits outside the places Tailwind scans, add it with @source in your CSS.
- Import it with import { LineReveal } from "@/components/dimaac/line-reveal" and give its parent a size: it fills it.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children (required) | string | - | The text to show. Plain text only (the lines are cut from it). |
as | "h1" | "h2" | "p" | "p" | The tag to render: a page title, a section title or a paragraph. |
stagger | number | 0.08 | Delay between one line starting and the next, in seconds. |
duration | number | 1 | How long each line takes to rise, in seconds. |
highlight | boolean | false | Fill the words in one by one as you scroll through the text. |
highlightColor | string | "#ff5a36" | The colour a word turns when it is filled in. Empty keeps the text colour. |
start | string | "top 85%" | When the reveal starts: "<element edge> <viewport edge>", e.g. "top 85%". |
scroller | string | HTMLElement | - | The scrolling element. Leave it out to use the page; give it a selector or an element for an inner scroller. |
className | string | "" | Extra classes: put the font size, weight and colour here. |
Use with AI
Agent prompt
Paste it into Claude Code, Cursor or any coding agent, or open it straight in a chat. It carries your Customize settings.
Add the Line Reveal component (<LineReveal />) from DiMaac UI to this project.
Docs: https://ui.dimaac.com/components/line-reveal
Source and dependencies (shadcn registry JSON): https://ui.dimaac.com/r/line-reveal.json
Install it with:
npx shadcn@latest add https://ui.dimaac.com/r/line-reveal.json
Then use it like this:
import { LineReveal } from "@/components/dimaac/line-reveal";
<LineReveal children={children} />
If the shadcn CLI isn't set up here, fetch the registry JSON above, write files[0].content to components/dimaac/line-reveal.tsx and install gsap, @gsap/react.
It uses Tailwind v4 classes, so make sure Tailwind scans that folder. Give its parent a size, keep light and dark mode working and keep its prefers-reduced-motion fallback.
Full index for agents: https://ui.dimaac.com/llms.txtFiles agents read
- llms.txt
- Every component and how to install it
- line-reveal.md
- This page as Markdown: install, props and the full source
- line-reveal.json
- The shadcn registry item
About
A heading or paragraph that is cut into lines and lifted out of a mask as it scrolls into view, the way editorial award sites open a page. Switch on highlight and the words start faint and fill in one by one as you scroll through the text. It uses GSAP SplitText with autoSplit, so the lines are cut again when the width changes or the web font loads, and everything is put back the way it was on unmount. The text stays real text: selectable, and read as one sentence by screen readers. It takes an optional scroller for inner scroll areas and shows the text as it is when the visitor prefers reduced motion.
- Installs to
components/dimaac/line-reveal.tsx- Uses
- gsap, @gsap/react
- Tags
- gsap, splittext, scrolltrigger, text, editorial