You can build a text reveal animation in Webflow that’s reusable, scalable, and easy to apply — to a single-line heading, a multi-line heading, or a button on hover — with a single action inside the animation timeline. The trick is to stop fighting Webflow’s native elements and reach for one you might have overlooked: the Custom Element.
I’ll start with the “old way” first, because it’s the version most people build, and it’s worth seeing exactly where it falls apart. Then I’ll rebuild the same effect the new way. When I timed myself, the old way took over six minutes per heading; the new way took under four — and, more importantly, the new one scales to longer and multi-line headings with almost no extra work.
The whole idea rests on a small shift in thinking: let a CSS transition do the animating, and let the Custom Element carry the per-letter timing as inline CSS. Once that clicks, the Webflow timeline shrinks to a single action.
How it works
The old way, and why it hurts. The classic setup masks a heading (overflow: hidden), splits it into one span per letter, sets each span to display: inline-block so it can take a transform, then builds a scroll-into-view interaction that moves every letter up with a hand-tuned delay. It works, but each letter needs its own action, longer headings force you back into the timeline to add more, and native Webflow headings can’t do multi-line reveals at all without an accessibility-hurting hack. There’s also a Webflow quirk worth knowing: two actions created from the same span get linked to the same target, so you build the initial condition from the last span to keep them independent.
The Custom Element. A Custom Element is a placeholder you drop on the canvas (Ctrl/Cmd + E → “custom element”) and then assign any HTML tag to via the settings panel. Set its Tag to h2 and it is an h2. Set another’s Tag to span and it becomes a span you can also give a Text value to. Crucially, a Custom Element accepts any custom attribute — including style, which lets you inject inline CSS. That single capability is what makes the new approach possible.
Why the animation shrinks to one action. In the new setup, the shared span class carries a transform transition (700ms, ease-out-cubic). Webflow animations act on CSS properties, so when your timeline changes a property that already has a matching transition, the transition does the animating — you only specify the end value, not a duration or easing. The stagger, meanwhile, comes from a transition-delay set individually on each span through its inline style attribute (0, 100, 200… milliseconds). So the timeline needs just an initial condition (push the span wrapper down to hide the letters) and one action (move the letters up by -100%). Everything else is CSS.
Here’s the finished markup for the heading “The New Way”, with Webflow’s own interaction attributes stripped out so you can see what you’re actually building:
<h2 class="new-way_heading">
<div class="h2_heading-line">
<div class="h2_span-wrapper">
<span class="h2_span" style="transition-delay: 0ms">T</span>
<span class="h2_span" style="transition-delay: 100ms">h</span>
<span class="h2_span" style="transition-delay: 200ms">e</span>
<span class="h2_span" style="transition-delay: 0ms"> </span>
<span class="h2_span" style="transition-delay: 300ms">N</span>
<span class="h2_span" style="transition-delay: 400ms">e</span>
<span class="h2_span" style="transition-delay: 500ms">w</span>
<span class="h2_span" style="transition-delay: 0ms"> </span>
<span class="h2_span" style="transition-delay: 600ms">W</span>
<span class="h2_span" style="transition-delay: 700ms">a</span>
<span class="h2_span" style="transition-delay: 800ms">y</span>
</div>
</div>
</h2>
Three things to read out of that. The delay lives in a literal style attribute on each span — in Webflow you add an attribute called style with the value transition-delay: 300ms, which is exactly the capability that makes a Custom Element worth reaching for. The spaces are their own spans holding a non-breaking space ( ) — a plain space would collapse between inline-blocks — and they sit at 0ms, so they don’t consume a step in the sequence: the eleven spans carry nine delays, 0 through 800ms. And the nesting is three deep: h2_heading-line is the mask (overflow: hidden), h2_span-wrapper is what the initial condition pushes down by 100%, and h2_span is what the single action moves up by -100%. On the published page Webflow adds its own data-w-id to each span and writes the transforms inline as the interaction runs — none of that is yours to author.
How to use it
-
Build the heading with Custom Elements. Add a Custom Element and set its Tag to
h2. Inside it, add adivblock as a heading line and give thatoverflow: hidden(it’s the mask). Inside the line, add anotherdivas the span wrapper. Inside the wrapper, add a Custom Element per letter (and per blank space), setting each one’s Tag tospanand its Text to the letter it represents. -
Style the spans once. Apply a single base class to every span with
display: inline-block(so transforms apply) and a transform transition of 700ms, ease-out-cubic. No combo classes per letter this time — one shared class is enough. -
Add the per-letter timing as inline CSS. On each span’s
styleattribute, settransition-delayto a stepped value —0ms,100ms,200ms, and so on. In the reference project the steps run from 0ms up to 800ms in 100ms increments, so the whole word lands within a second. This is the stagger, and it lives entirely outside the timeline. -
Build the one-action animation. Select the heading, add a Scroll into view interaction. Initial condition: move the span wrapper down by
100%(it hides below the overflow-hidden line). Then one action: move the spans up by-100%. No duration, no delay, no easing on the action itself — the transition and the inline delays handle all of it. -
Extend to multi-line. Duplicate the heading line (wrapper + spans) for each additional line, and reuse the same animation untouched. Only adjust the
transition-delayvalues — e.g. offset the second line’s letters by50msso the two lines cascade. -
Adapt for a button-on-hover reveal. Give the button’s text wrapper
overflow: hiddenandposition: relative. Put two copies of the text inside; set the second toposition: absolutewith atopoffset of100%so it starts hidden below. Split both into delayed spans as above, then add a hover interaction with a single action moving the spans up by-100%, and a hover-out action returning them to0%.
Note: you build this element by element in the Designer rather than pasting the markup — the HTML above is what you should end up with, not something you can paste into an Embed (an Embed can’t hold a heading you want Webflow interactions to target). Clone the project if you’d rather start from a working copy.