Next.js Text Reveal Animation: Line-by-Line and Word-by-Word
Published: September 12, 2026 · 5 min read
Reveal headline text line by line or word by word in Next.js: CSS animation-delay, Framer Motion word splits, masked line reveals, and SEO and accessibility.
Split a Next.js text reveal animation by line or word
You want the headline in your Next.js hero to fade in piece by piece. You tried letter by letter like a site you admired, and it became hard to read and broke when wrapping on mobile. This guide starts from there.
The answer: split a text reveal by line or by word, not by letter. Letter-by-letter makes people wait to read the copy and is unkind to screen readers. For a landing page hero, revealing label, headline, then body in sequence already reads as motion.
What you will get from this article:
Block-by-block reveal with CSS animation-delay only
A Motion (formerly Framer Motion) component that reveals word by word
A masked line reveal for hero headlines
How to split headlines in languages without spaces, like Japanese
SEO, screen reader, and reduced motion considerations
Why letter-by-letter text animation hurts readability
Letter-level effects are for showing, like a film's opening titles. A landing page headline is there to be read, so they clash. Three problems show up.
Nobody knows what the headline says until it finishes, and many scroll away first
Absolutely positioning or fixing the width of each letter breaks wrapping when the viewport resizes
Some screen readers read each split piece separately, losing the meaning
Splitting by line or word avoids all three and keeps the total time short. As a rule, finish revealing first-view text within one second.
Reveal text blocks in sequence with CSS animation-delay
Try this first: do not split words at all, just delay each block. Label, h1, body, and button rise in 0.12 seconds apart. For most landing pages, this alone feels alive.
No JavaScript, so it runs as a Server Component. Add one keyframes rule to globals.css.
@keyframes reveal-up {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}Offset delays with Tailwind CSS arbitrary values
animate-[reveal-up_0.6s_ease-out_0.12s_both] packs a whole CSS animation setting into one class. Here is what each part means:
reveal-up: the name of the keyframes you just added
0.6s: how long the motion takes
ease-out: a curve that slows to a stop
0.12s: the wait before it starts
both: keep the start and end states before and after the motion
The trailing both keeps the element transparent during the delay and visible after the animation ends.
export function HeroText() {
return (
<div className="mx-auto max-w-3xl px-6 pt-32 text-center">
<p className="animate-[reveal-up_0.6s_ease-out_both] text-sm font-medium text-indigo-600 motion-reduce:animate-none">
New features this spring
</p>
<h1 className="mt-4 animate-[reveal-up_0.6s_ease-out_0.12s_both] text-4xl font-semibold tracking-tight motion-reduce:animate-none md:text-6xl">
Ship a landing page that moves, today
</h1>
<p className="mt-6 animate-[reveal-up_0.6s_ease-out_0.24s_both] text-lg text-zinc-600 motion-reduce:animate-none">
Pick sections, paste them in, swap the copy. Done.
</p>
<a
href="#start"
className="mt-10 inline-block animate-[reveal-up_0.6s_ease-out_0.36s_both] rounded-full bg-zinc-900 px-6 py-3 text-white motion-reduce:animate-none"
>
Start free
</a>
</div>
);
}When a block-by-block text reveal fits
Use it when the headline runs two or more lines, or when copy changes often. You never think about how words split, so swapping text cannot break it.
Reveal text word by word with Framer Motion
When the headline itself should move, split it into words, wrap each in a span, and offset the delays. Motion keeps the delay and motion in one place.
The component below takes an array of pieces. For English, split on spaces. For languages without spaces, split by phrase yourself.
"use client";
import { Fragment } from "react";
import { motion, useReducedMotion } from "motion/react";
export function SplitReveal({
parts,
spaced = false,
className,
}: {
parts: string[];
spaced?: boolean;
className?: string;
}) {
const reduce = useReducedMotion();
return (
<h1 className={className}>
{parts.map((part, i) => (
<Fragment key={i}>
<motion.span
className="inline-block"
initial={reduce ? false : { opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: i * 0.06, ease: "easeOut" }}
>
{part}
</motion.span>
{spaced && i < parts.length - 1 ? " " : null}
</Fragment>
))}
</h1>
);
}Text without spaces: split by phrase
Japanese and Chinese have no spaces between words, so split(" ") does nothing, and letter-by-letter should be avoided. Split the headline by hand into short meaningful phrases or at commas.
Because each span is inline-block, lines wrap only between pieces. Phrase splits also stop phones from breaking a word in the middle. Some screen readers may still pause between the split spans. If that matters, put the full sentence in an aria-label on the h1 and add aria-hidden to the split spans.
// English: split on spaces, and spaced puts the spaces back
<SplitReveal
parts={"Ship a landing page that moves".split(" ")}
spaced
className="text-4xl font-semibold tracking-tight md:text-6xl"
/>
// Japanese: split by phrase by hand
<SplitReveal
parts={["動きのある", "LP を、", "今日のうちに", "公開する"]}
className="text-4xl font-semibold tracking-tight md:text-6xl"
/>Keep the stagger around 0.06 seconds
With six pieces at 0.06 seconds each, the last one starts at 0.3 seconds. At 0.15 seconds each, the full reveal takes over a second and feels slow. The more pieces you have, the shorter each delay should be.
A masked line-by-line text reveal for heroes
A common effect for big headlines: each line slides up from below as if rising out of a slot. Wrap each line in an overflow-hidden box and move the text from below the box to its normal position. While outside the box it is clipped, so it looks masked.
whileInView plays it when the heading enters the viewport, so it also works for section headings further down.
"use client";
import { motion } from "motion/react";
export function LineMaskReveal({ lines }: { lines: string[] }) {
return (
<h2 className="text-4xl font-semibold leading-tight md:text-6xl">
{lines.map((line, i) => (
<span key={i} className="block overflow-hidden pb-1">
<motion.span
className="block"
initial={{ y: "110%" }}
whileInView={{ y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, delay: i * 0.1, ease: [0.22, 1, 0.36, 1] }}
>
{line}
</motion.span>
</span>
))}
</h2>
);
}Keep each masked text line short
If a line in lines is long, it wraps again on phones. The motion does not break, but two visual lines rise together and the effect weakens. Split at a length that fits one line on mobile.
pb-1 leaves room for descenders like g and y so the box does not clip them.
Text animation, SEO, and screen readers
Keep the text in the HTML from the start
Every example here outputs the headline text in server-rendered HTML. Even when Motion's initial makes it transparent, the words are in the HTML, so search engines and screen readers get them.
Avoid adding characters to innerText one at a time inside useEffect. The heading is empty in the HTML, and its content is harder to understand.
Show the full text immediately for reduced motion
Users who turn on reduced motion in their device settings see the full text at once. The CSS version does this with motion-reduce:animate-none, and SplitReveal with useReducedMotion, a hook that reads that setting.
To handle it site-wide, wrap your layout in MotionConfig, a component that sets motion options for everything inside it, with reducedMotion="user".
Finish the first view within one second
Elements at opacity 0 are excluded from Largest Contentful Paint (LCP), so measurement waits until the headline becomes visible. Finish first-view headlines, delays included, within one second.
Use a finished hero with a text reveal

Masked lines, knockout type, timing matched to background motion: a hero with all of that takes time to tune even when you combine the code above.
DesignLayer is a catalog of human-built React + Tailwind motion sections you can live-preview in the browser, then copy. Browsing needs no account and no card. https://design-layer.com/en/components
A free account copies the 10 Free Sample components with no monthly limit on a free account, plus 1 video prompt a month. Free cannot connect MCP. The rest of the catalog is the 30-Day Build Pass ($19 once, no auto-renewal), Lifetime Access ($79 once, first 100 buyers), or Plus ($9/month). One hero today fits the quota. For building the whole hero, see https://design-layer.com/en/articles/nextjs-animated-hero-section-guide
Next.js text reveal animation: common questions
Not if the heading text is in the HTML from the start, which is true for every approach here. Avoid writing text in later on the client.
Summary: building a text reveal animation in Next.js
Split text by line or word (phrase for Japanese), never by letter
Start with plain CSS delays for label, headline, and body
To move the headline itself, stagger pieces by about 0.06 seconds with Motion
A masked line reveal is a box that hides overflow plus a vertical move
Keep text in the HTML, and show it at once for reduced motion
Paste HeroText and adjust the delays until the tempo feels right. For Motion basics, see https://design-layer.com/en/articles/nextjs-framer-motion-guide and for the full hero, https://design-layer.com/en/articles/nextjs-animated-hero-section-guide


