Line reveal
Headings that come in line by line. As a heading enters the window, each of its lines rises from behind a mask, one after the other, and lands sharp.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialBefore the script runs, a marked heading is hidden withvisibilityand keeps its room, so nothing below it moves. It shows by itself after four seconds if the script never comes, and at once without JavaScript, through the noscript line the paste carries inside its Embed. The Designer canvas runs no Embed: there the headings are always plain and editable. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Use it on your own elements
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the element on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-line-reveal Marks a heading whose lines rise one after the other as it comes into view. It takes no value: leave the value field empty. It goes on the heading itself, an h1, h2 or h3, or on any block of text; the script finds the lines on screen, and bold words and links inside it stay as they are. data-line-reveal-skip On an inline element inside a marked heading: its words are left out of the split and stand still while the rest rises. [ Optional ] data-line-reveal-state Written by the engine on the heading: ready while it waits hidden, playing, done at rest, static under reduced motion. Never written by hand. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same element. An attribute always wins over the defaults written in the code.Attribute Default What it is data-line-reveal-duration 1.1 Time one line takes from its hidden pose to rest, in seconds. data-line-reveal-stagger 0.08 Delay between the start of one line and the start of the next, in seconds. data-line-reveal-ease expo.out Shape of the rise: a CSS easing (cubic-bezier(...), ease-out) or a GSAP name the engine translates (expo.out, power1.out to power4.out, sine.out, circ.out, none). Options: any CSS easing or one of those names. data-line-reveal-delay 0 Wait before the first line starts, in seconds, once the heading is in view. data-line-reveal-offset 0.12 How far over the bottom edge of the window the heading has to come before it plays, as a share of the window height, 0 to 0.9. data-line-reveal-replay false Whether the heading plays again each time it comes back into view. Options: false (once, then it stays), true (it waits hidden again once it has fully left the window).
Two files, no build step.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
LineReveal.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-line-reveal-duration 1.1 Time one line takes from its hidden pose to rest, in seconds. A snap: the lines pop up A slow, stately rise data-line-reveal-stagger 0.08 Delay between the start of one line and the start of the next, in seconds. The lines rise almost as one block Each line clearly after the other, a cascade data-line-reveal-ease expo.out Shape of the rise: a CSS easing (cubic-bezier(...), ease-out) or a GSAP name the engine translates (expo.out, power1.out to power4.out, sine.out, circ.out, none). Options: any CSS easing or one of those names. data-line-reveal-delay 0 Wait before the first line starts, in seconds, once the heading is in view. Plays the moment it enters Lets another element go first; three headings side by side with 0, 0.1 and 0.2 come in as a row data-line-reveal-offset 0.12 How far over the bottom edge of the window the heading has to come before it plays, as a share of the window height, 0 to 0.9. It plays as soon as it enters at the bottom It waits until it has climbed higher up the screen data-line-reveal-replay false Whether the heading plays again each time it comes back into view. Options: false (once, then it stays), true (it waits hidden again once it has fully left the window). -
Drive it from JavaScript
When a heading arrives after the page has loaded, when it has to play again from code, or when the page reacts to it landing.const element = document.querySelector('[data-line-reveal]'); element.addEventListener('line-reveal:done', (e) => { console.log(e.detail); }); const heading = LineReveal.mount(element); // the heading auto() mounted, or a new one heading.replay(); // back to its hidden pose, then played again heading.lines(); // the number of lines read on screen // Headings added after the page loaded: mount everything marked inside it. LineReveal.auto(container); // When the heading leaves the page: heading.destroy(); // the words go back exactly as they were
-
Fit it to your page
- One attribute, on the heading itself. Put
data-line-revealon theh1,h2,h3, or any block of text meant to rise line by line; it takes no value. The script wraps each word where it stands, finds the lines on screen and plays them as the heading enters the window. Nothing to split by hand. In Webflow, select the Heading, open the Settings panel and add the custom attributedata-line-revealwith an empty value. - Inline markup stays itself. A
strong, anem, aspanor a link inside the heading is the same element afterwards, with its own class and listeners; the words inside it are wrapped in place. A link keeps its name for assistive technology through anaria-labelthe script adds anddestroy()takes off. Putdata-line-reveal-skipon an inline element to keep its words out of the split: they then stand still while the rest rises. - One setting for every heading of the page, without opening the file: declare
window.LineRevealSettings = { stagger: 0.12 }in a script placed beforeline-reveal.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-line-reveal-*attribute on the heading itself (data-line-reveal-delay="0.2"), so one heading can still differ from the page-wide setting. - Before the script, the heading keeps its room unseen. A marked heading is hidden with
visibility, neverdisplay, until the script has run, so nothing below it moves when it shows. If the script never runs, the heading shows by itself after four seconds; without JavaScript at all, thenoscriptline at the top of snippet.html shows it at once. Keep that line on the page. - In the Webflow Designer the headings are plain. The canvas runs no Embed and applies none of its styles, so a marked heading is always there, whole and editable; the hidden pose and the rise only exist on the published page and in the preview.
- A heading already above the window when the page opens (a reload half way down) is shown at rest, not played behind the reader's back.
- **
prefers-reduced-motion**: the heading stands at rest from the start and never moves. The words stay wrapped, which cannot be seen. - Resize and late fonts: the lines are read again on every change of the heading's size and once each web font lands, so a heading always rises by the lines it shows. A resize during the rise waits for it to end.
- States, on the heading, for a style or a test:
data-line-reveal-stateisready(hidden, waiting),playing,doneorstatic(reduced motion). Each change fires a bubbling event of the same name:line-reveal:ready,line-reveal:playing,line-reveal:done,line-reveal:static, whosedetailis{ element, lines }. - From JavaScript:
LineReveal.mount(el, overrides)returns the instance, which carrieselement,settings(the values it resolved),state(),lines()(the number of lines read),split()(read the lines again),play(),show()(to rest at once, no motion),reset()(back to the hidden pose),replay()anddestroy().LineReveal.auto(root)mounts every marked heading insideroot,LineReveal.replay(root)plays them again,LineReveal.destroy(root)takes them all back.destroy()unwraps every word, joins the text nodes again and puts back thestyleandaria-labelthe heading had, so the markup is the one of snippet.html and mounting again starts clean. In React,mountinuseEffectanddestroyin its cleanup. - The words of the markup are the preview's, and they ship as they are: pasted, the page reads the way the preview did. Put the user's own headings in their place whenever they want, in the page's language.
- On Webflow, add the fonts first. Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: the paste names them, and the Designer drops a font the site does not have.
- One attribute, on the heading itself. Put
-
Avoid the pitfalls
- **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a heading that already carries the effect returns the existing one untouched. A page-wide block therefore goes inwindow.LineRevealSettings, whichauto()reads itself, or the heading is taken back withdestroy()and mounted again. - Text added to a heading after it mounted is not split. Change the words, then
destroy()andmount()it again. - **A heading painted through
background-clip: text** (gradient text) loses it: every word becomes a box of its own. Paint the words, not the heading. - The hidden pose lives in the stylesheet. Loaded late, or at the foot of a heavy page, a heading high on the page can show for a moment before the rule that hides it arrives, then hide and rise. In a Webflow paste the rule travels in the Embed, after the headings; when the first heading of a long page flickers, copy the first rule of line-reveal.css (the one that sets
visibility: hidden) into the site's head code as well. - Whitespace kept as typed is not covered. The split heading does not wrap by itself, only its spaces do; a heading in
white-space: preorpre-wrapbreaks where the engine did not look. - **
overflow: hiddenon the heading clips the accents** that the windows let show above and below each line. Leave the heading's overflow visible.
- **Settings handed to
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Line reveal is a text animation for Webflow and vanilla JavaScript, with no library. Also called line by line text reveal, line reveal animation or responsive text line reveal.
Headings that come in line by line. As a heading enters the window, each of its lines rises from behind a mask, one after the other, and lands sharp. The lines are the ones on screen: the script reads where the words fell at the width the heading has, and reads them again when the width or the font changes, so a heading of two lines on a desk is a heading of five on a phone and still rises line by line. Nothing is split by hand and nothing is cloned: bold, links and any inline markup stay the elements they were, around their own words. Each word rises straight up through a window of its own, fast then settling, so a line comes up as one clean edge. Before the script runs a marked heading keeps its room unseen, so nothing below it moves; without JavaScript it shows at once, and a screen reader reads the whole heading as one name. Under reduced motion the heading simply stands. Pattern type split text reveal, line mask reveal.
Updated
Questions
Select the Heading, open the Settings panel and add the custom attribute data-line-reveal with an empty value. The script wraps each word where it stands and finds the lines on screen. In the Designer the heading stays whole and editable: the hidden pose and the rise only exist on the published page and in the preview.
No. It needs no library: the lines are read from the layout, each word sits behind a clip-path mask and rises through a CSS transition, and an IntersectionObserver starts it. The ease setting still accepts GSAP names such as expo.out, the default, which the engine translates.
Yes. The lines are read again on every change of the heading's size and once each web font lands, so a heading of two lines on a desktop and five on a phone still rises line by line. A resize during the rise waits for it to end.
data-line-reveal-duration is the time one line takes to land, 1.1 seconds by default, and data-line-reveal-stagger is the delay between two lines, 0.08 by default: lower and the lines rise almost as one block, higher and they cascade. data-line-reveal-delay holds the first line back, so three headings side by side with 0, 0.1 and 0.2 come in as a row.
The heading still shows. It is hidden with visibility until the script runs, shows by itself after four seconds if the script never does, and the noscript line of the snippet shows it at once without JavaScript. Under prefers-reduced-motion it stands at rest from the start and never moves.