Word loop
A heading with one word that turns. The word sits in a slot on the heading's own baseline, and every few seconds it gives way to the next one of a list: the letters of the word that leaves rise one after the other out of a mask, the letters of the next one rise from below on the same beat, and the slot changes width at the same time, so the rest of the line glides to its new place instead of jumping.
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 tutorialThe slot has no width of its own at rest: it is as wide as its word, so nothing is frozen and nothing has to be styled. Leave the heading and the span withoutoverflow: hiddenand withouttext-wrap: balance: the first would cut the rule, the second would move the line breaks at every word. Keep the word on a line it cannot push to the next one, as the three lines of the preview do. -
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-word-loop Marks the word that cycles: a span around one word of a heading. It takes no value: leave the value field empty. The text of the span is the first word, and the script builds the slot, the letters and the rule inside it; the heading around it stays as it is. data-word-loop-words The other words of the list, separated by |, on the marked span: printers|architects|binders. The span shows its own text first, then each of them in turn, and starts again. In code the words can also be written as children of the span, one element per word. With neither, the word stands still over its rule. [ Optional ] data-word-loop-state Written by the engine on the span: idle while a word rests and the clock runs, changing during a change, paused off screen, under the pointer or in a hidden tab, 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-word-loop-color inherit Ink of the word that cycles; inherit keeps the colour of the heading. Options: inherit, or any CSS colour to set the word apart from the rest of the line; taken out of this block, --word-loop-color on the span takes over. data-word-loop-rule #ffab1a Colour of the rule under the word. Options: any CSS colour; taken out of this block, --word-loop-rule on the span takes over. data-word-loop-interval 2.4 Time from the start of one change to the start of the next, in seconds; the first word leaves after half of it, and a word always rests at least 0.3 s. data-word-loop-duration 0.7 Time one letter takes to travel, and the slot to change width, in seconds. data-word-loop-stagger 0.03 Delay between two neighbouring letters, left to right, in seconds; the whole cascade never lasts longer than duration. data-word-loop-direction up Way the letters travel. Options: up (the old word rises out, the new one rises from below), down (the reverse). data-word-loop-rule-width 0 Thickness of a rule under the word, in px, 0 by default. data-word-loop-pause-on-hover 0 Whether the loop waits while a mouse is on the word. Options: 0 (it never waits, the default), 1 (it waits, and starts again when the pointer leaves).
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
WordLoop.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-word-loop-color inherit Ink of the word that cycles; inherit keeps the colour of the heading. Options: inherit, or any CSS colour to set the word apart from the rest of the line; taken out of this block, --word-loop-color on the span takes over. data-word-loop-rule #ffab1a Colour of the rule under the word. Options: any CSS colour; taken out of this block, --word-loop-rule on the span takes over. data-word-loop-interval 2.4 Time from the start of one change to the start of the next, in seconds; the first word leaves after half of it, and a word always rests at least 0.3 s. A restless heading, hard to read under 1.5 Each word has time to be read twice data-word-loop-duration 0.7 Time one letter takes to travel, and the slot to change width, in seconds. A snap, the letters flick past A slow roll, each letter can be followed data-word-loop-stagger 0.03 Delay between two neighbouring letters, left to right, in seconds; the whole cascade never lasts longer than duration. At 0 the word moves as one block A wave that runs along the word data-word-loop-direction up Way the letters travel. Options: up (the old word rises out, the new one rises from below), down (the reverse). data-word-loop-rule-width 0 Thickness of a rule under the word, in px, 0 by default. At 0 there is no rule at all A bar under the word rather than a hairline data-word-loop-pause-on-hover 0 Whether the loop waits while a mouse is on the word. Options: 0 (it never waits, the default), 1 (it waits, and starts again when the pointer leaves). -
Drive it from JavaScript
When a word arrives after the page has loaded, when the page drives the changes itself, or when it reacts to the word on show.const element = document.querySelector('[data-word-loop]'); element.addEventListener('word-loop:change', (e) => { console.log(e.detail); }); const loop = WordLoop.mount(element); // the span auto() mounted, or a new one loop.next(); // the next word now loop.go(0); // back to the first word loop.pause(); // the clock stops; loop.play() starts it again element.addEventListener('word-loop:change', (e) => { console.log(e.detail.word); // the word coming in }); // When the heading leaves the page: loop.destroy(); // the span goes back exactly as it was
-
Fit it to your page
- One attribute, on the word itself. Wrap the word that cycles in a
spaninside the heading and putdata-word-loopon that span; it takes no value. The text of the span is the first word. The others go indata-word-loop-words, separated by|:<span data-word-loop data-word-loop-words="printers|architects">designers</span>. In Webflow, select the word in the Heading, wrap it in a span, open the Settings panel and add the two custom attributes. - In code, the list can be written as children instead:
<span data-word-loop>designers<i>printers</i><i>architects</i></span>. Any element does, one per word; the stylesheet hides them until the script has run, so the heading reads with its first word alone. Both forms can be mixed: the children come first, then the attribute. - A word can be several words.
print shopsis one entry of the list, and its space travels like a letter. - One setting for every loop of the page, without opening the file: declare
window.WordLoopSettings = { interval: 3 }in a script placed beforeword-loop.js. The three levels, least specific first, areDEFAULTS, then that object, then adata-word-loop-*attribute on the span itself (data-word-loop-rule="#ffab1a"), so one heading can still differ from the page-wide setting. - The rule sits under the letters, not under the line. Its gap is
--word-loop-rule-gap, a custom property on the span or on any parent, in em of the heading, 0.1em by default;ruleWidthis 0 by default, so the word stands with no rule; 1 draws a hairline. - What starts it and what stops it. The loop starts once the fonts are in and the word is on screen. It waits while the heading is off screen, while the tab is hidden and, with
pauseOnHover, while a mouse is on the word. A click or a tap on the word moves on to the next one at once; asked during a change, the move is kept and played as soon as that change lands. - **
prefers-reduced-motion**: the word still changes on the same clock, but at once, with no letter moving and no width animated, and the rule stands still. - Without the script, and in the Webflow Designer, the heading reads with its first word, as written. The canvas runs no Embed.
- For a screen reader the span holds the word on show as plain text, and the animated letters are hidden from it, so the heading reads as one sentence, never letter by letter. The text is not a live region: a change is not announced.
- States, on the span, for a style or a test:
data-word-loop-stateisidle(a word rests, the clock runs),changing,paused(off screen, hidden tab, pointer on it,pause()) orstatic(reduced motion). - Events, both bubbling, with
detail{ element, index, word, previous }:word-loop:changeas a change starts,word-loop:doneonce the last letter has landed and the slot has its new width. - From JavaScript:
WordLoop.mount(el, overrides)returns the instance, which carrieselement,settings(the values it resolved),words(),index(),state(),next(),prev(),go(index),pause(),play(),refresh()(lands a change in flight at once) anddestroy().WordLoop.auto(root)mounts every marked word insideroot,WordLoop.replay(root)moves them all on to their next word,WordLoop.destroy(root)takes them all back.destroy()puts the original markup back, so 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 heading and list 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 word itself. Wrap the word that cycles in a
-
Avoid the pitfalls
- The engine animates a width, not a line break. When the longest word no longer fits on the line, the browser sends the slot, or the words after it, to the next line, and that happens in one step. Give the cycling word a line that holds its longest entry: a line of its own, or the three block lines of the preview. Only a word wider than the whole heading breaks inside the slot, and the slot then changes height with the same ease as its width.
- **
text-wrap: balanceon the heading moves every line at every word.** The browser balances again each time the slot changes width. Leave the heading on the default wrap. - **
overflow: hiddenon the heading or on the span cuts the rule**, which hangs under the letters, and the accents of capitals. Leave both visible. - **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a span that already carries the effect returns the existing one untouched. A page-wide block goes inwindow.WordLoopSettings, whichauto()reads itself, or the span is taken back withdestroy()and mounted again. - A list changed after the span mounted is not read again. Change the attribute or the children, then
destroy()andmount(). - The word is plain text. Markup inside an entry, a bold or a link, is read as its text only; and a word painted through
background-clip: textloses it, since every letter becomes a box of its own. - Letters are split one by one, so the pairs the font kerns and its ligatures are set apart, at rest as in flight: the word is the same width all along, which is what keeps the slot from jumping. Scripts that join their letters, Arabic for one, are not covered; nor is a right to left heading, the slot being laid out from the left.
- The span must stay inline. A class that makes it a block or a flex item takes the slot off the baseline of its line.
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
Word loop is a text animation for Webflow and vanilla JavaScript, with no library. Also called rotating text animation, rotating words or text loop.
A heading with one word that turns. The word sits in a slot on the heading's own baseline, and every few seconds it gives way to the next one of a list: the letters of the word that leaves rise one after the other out of a mask, the letters of the next one rise from below on the same beat, and the slot changes width at the same time, so the rest of the line glides to its new place instead of jumping. A thin rule under the word is wiped off and drawn again from the left at each change. Each word is measured as it comes, once the fonts are in, and at rest the slot has no width of its own, so nothing is ever frozen at a stale size. The loop waits while the heading is off screen, in a hidden tab, and while the pointer is on the word; a click or a tap moves on at once. A word longer than its line breaks inside the slot instead of pushing the page wider. Under reduced motion the word still changes, at once, and the rule stands. Pattern type rotating text, word rotate, flip words, text loop.
Updated
Questions
Select the word in the Heading, wrap it in a span, and add two custom attributes to that span: data-word-loop, with no value, and data-word-loop-words with the other words separated by |, for instance data-word-loop-words="printers|architects". The text of the span is the first word. In the Designer the heading reads with that first word, since the canvas runs no Embed.
No. There is no library and no build: the letters are split into masked cells and moved with the Web Animations API. The width of each word is measured as it comes, once the fonts are in, and the slot is animated to it so the rest of the line glides.
Set data-word-loop-interval, the time in seconds from the start of one change to the start of the next, 2.4 by default. Under 1.5 the heading is hard to read, and a higher value leaves each word on show longer. data-word-loop-duration, 0.7 by default, is the time one letter takes to travel and the slot to change width.
Because the engine animates a width, not a line break: when the longest word no longer fits, the browser moves it to the next line in one step. Give the cycling word a line that holds its longest entry, a line of its own for instance. Also leave text-wrap: balance off the heading, or every line moves at every word.
Yes. The span holds the word on show as plain text and the animated letters are hidden from it, so the heading is read as one sentence and never letter by letter. The text is not a live region, so a change of word is not announced.