AnimationsIntermediateApril 8, 2026
SplitText Line Reveal
Paragraph text is split into individual lines. Each line slides up from below its overflow-hidden mask container as the section enters the viewport. Uses a manual canvas-based line splitter (no paid plugin required).
View Full Demo →Preview
No-5 Studio is a motion-forward design studio. We build websites that communicate confidence, craft, and intention through every interaction.
Our process starts with animation. Every project begins with an animation language — a set of principles that define how your brand moves through the digital world.
Source
demo.jsx
import SplittextLineReveal from "./index.jsx";
import styles from "./demo.module.css";
const PARAGRAPHS = [
"No-5 Studio is a motion-forward design studio. We build websites that communicate confidence, craft, and intention through every interaction.",
"Our process starts with animation. Every project begins with an animation language — a set of principles that define how your brand moves through the digital world.",
];
export default function SplittextLineRevealDemo() {
return (
<div>
{/* Hero — text component starts below the fold */}
<section className={styles.hero}>
<h1 className={styles.heroTitle}>
<span>No-5</span>
<span>Studio</span>
</h1>
<p className={styles.heroSub}>Motion-forward design</p>
</section>
{/* Component — lines reveal as you scroll into this section */}
<section className={styles.content}>
<p className={styles.contentLabel}>About</p>
<SplittextLineReveal paragraphs={PARAGRAPHS} />
</section>
<div className={styles.spacer} />
</div>
);
}
index.jsx
"use client";
import { useEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import styles from "./styles.module.css";
gsap.registerPlugin(ScrollTrigger);
// Manual text split — splits text into lines by word-wrapping into a hidden container
function splitIntoLines(text, containerWidth, fontSize) {
const words = text.split(" ");
const lines = [];
let currentLine = "";
// Use canvas to measure text width
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
ctx.font = `${fontSize}px -apple-system, sans-serif`;
for (const word of words) {
const test = currentLine ? `${currentLine} ${word}` : word;
if (ctx.measureText(test).width <= containerWidth) {
currentLine = test;
} else {
if (currentLine) lines.push(currentLine);
currentLine = word;
}
}
if (currentLine) lines.push(currentLine);
return lines;
}
export default function SplitTextLineReveal({ paragraphs = [] }) {
const containerRef = useRef(null);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const paras = el.querySelectorAll(`.${styles.para}`);
paras.forEach((para) => {
const text = para.textContent;
const width = para.getBoundingClientRect().width;
const fontSize = parseFloat(getComputedStyle(para).fontSize);
const lines = splitIntoLines(text, width, fontSize);
para.innerHTML = lines
.map(
(line) =>
`<span class="${styles.lineOuter}"><span class="${styles.lineInner}">${line}</span></span>`
)
.join("");
const lineEls = para.querySelectorAll(`.${styles.lineInner}`);
gsap.from(lineEls, {
yPercent: 105,
duration: 1,
stagger: 0.07,
ease: "power3.out",
scrollTrigger: {
trigger: para,
start: "top 80%",
},
});
});
return () => ScrollTrigger.getAll().forEach((t) => t.kill());
}, []);
return (
<div ref={containerRef} className={styles.container}>
{paragraphs.map((text, i) => (
<p key={i} className={styles.para}>
{text}
</p>
))}
</div>
);
}
demo.module.css
.hero {
height: 100vh;
background: #0d0d0d;
color: #ffffff;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 3rem;
}
.heroTitle {
display: flex;
flex-direction: column;
font-size: clamp(4rem, 12vw, 10rem);
font-weight: 700;
letter-spacing: -0.04em;
line-height: 0.9;
margin-bottom: 1.5rem;
}
.heroSub {
font-size: 0.8125rem;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.3;
}
.content {
background: #ffffff;
padding: 4rem 3rem;
}
.contentLabel {
font-size: 0.75rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #9b9b9b;
margin-bottom: 2.5rem;
}
.spacer {
height: 30vh;
background: #ffffff;
}
styles.module.css
.container {
max-width: 600px;
margin: 0 auto;
padding: 4rem 2rem;
display: flex;
flex-direction: column;
gap: 2rem;
}
.para {
font-size: 1.125rem;
line-height: 1.65;
color: #1a1a1a;
}
/* Each line is wrapped in these during JS split */
.lineOuter {
display: block;
overflow: hidden;
}
.lineInner {
display: block;
}
Dependencies
gsap