Word Rotate <mv-word-rotate>
Inline rotating word for hero sentences (slide, 3D flip, blur or fade, letter by letter) whose width glides so the sentence never jumps. Accessible by default: screen readers hear one stable sentence listing every option, it holds on hover and on focus of a wrapping link, pauses off-screen and in hidden tabs, and stays on one word under reduced motion.
| Category | Animated text |
|---|---|
| Type | Web Component (<mv-word-rotate>) |
| Status | stable |
| Keywords | rotating-text, flip-words, text-loop, hero, headline, inline, layout-animation, accessible |
When to use
- A hero headline should cycle one inline word, like Build for designers, developers or teams
- The rotating word should sit in a tinted pill whose width animates between words
- A sold or public page needs a rotating word that screen readers hear once, not on every change
- The word loop must be driven from script with next, previous or an index
Avoid when
- A standalone word should melt into the next with a gooey blur → use Morphing Text instead
- Whole phrases should be typed and deleted letter by letter → use Typewriter instead
Install
node scripts/add.mjs word-rotate --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["word-rotate"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/word-rotate/word-rotate.js, components/word-rotate/word-rotate.css.
Usage
Quick start, the smallest working markup:
<p>Build for <mv-word-rotate words="designers, developers, teams"></mv-word-rotate></p>Canonical markup, to start from and customize with attributes, data-* and CSS variables:
<div style="display:grid;gap:2rem;width:min(100%,46rem);margin-inline:auto;text-align:center">
<h2 style="margin:0;font:700 clamp(1.9rem,4.6vw,3.2rem)/1.15 var(--mv-font-display);letter-spacing:-.035em">
Build for
<mv-word-rotate data-variant="pill" words="designers, developers, teams" style="--mv-word-rotate-color:var(--mv-accent-fg)"></mv-word-rotate>
<br>without the all-nighters.
</h2>
<div style="display:grid;gap:.9rem;font-size:clamp(1.1rem,2.4vw,1.45rem);font-weight:600;letter-spacing:-.02em">
<p style="margin:0">Interfaces that feel <mv-word-rotate effect="flip" interval="2200" words="fast, reliable, elegant, accessible" colors="var(--mv-accent), var(--mv-info), var(--mv-success), var(--mv-warning)"></mv-word-rotate></p>
<p style="margin:0">From prototype to <mv-word-rotate effect="blur" interval="2400" conjunction="and" words="production, launch, growth" style="--mv-word-rotate-color:var(--mv-accent)"></mv-word-rotate></p>
<p style="margin:0;color:var(--mv-fg-muted)">Ships as <mv-word-rotate effect="slide" direction="down" interval="1800" words="HTML, React, Vue, Svelte, Astro" style="--mv-word-rotate-color:var(--mv-fg);font-family:var(--mv-font-mono)"></mv-word-rotate>, zero friction.</p>
<p style="margin:0;color:var(--mv-fg-muted)">Looks <mv-word-rotate effect="fade" interval="2000" words="crisp, smooth, clean" style="--mv-word-rotate-color:var(--mv-fg)"></mv-word-rotate> by default.</p>
</div>
<p style="margin:0;font-size:.85rem;color:var(--mv-fg-muted)">Hover a word to hold it. Screen readers hear each sentence once, with every option.</p>
</div>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
words | string | Words separated by commas (or |). Otherwise: one child per word. | |
effect | slide | flip | blur | fade | slide | Transition between two words; letters enter one after another. |
interval | number (ms) | 2600 | How long each word is shown. |
duration | number (ms) | 560 (680 for flip) | Duration of the transition and of the width glide. |
stagger | number (ms) | 18-30 (0 for fade) | Delay between letters. |
easing | CSS easing | var(--mv-ease-out) | Letter easing curve (tokens accepted). |
direction | up | down | up | Slide / flip direction. |
colors | list of CSS colors | One color per word, cycled. | |
conjunction | or | and | none | or | How the screen reader sentence joins the words: “designers, developers, or teams”. Uses the page language (lang). |
label | string | Replaces the screen reader sentence entirely. | |
paused | boolean | Pauses the rotation. It also holds by itself on hover, on focus of a wrapping link or button, off-screen and in hidden tabs. | |
data-variant | pill | Tinted pill whose width animates. | |
data-held | boolean | Set by the component while hover or focus holds the word (for styling). |
Properties
| Name | Type | Description |
|---|---|---|
index | number | Current word (settable, wraps around; assignable before the element is defined). |
Methods
| Name | Description |
|---|---|
next() | Next word. |
previous() | Previous word. |
Events
| Name | Description |
|---|---|
mv-change | New word shown (detail.index, detail.word). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-word-rotate-color | inherit | Word color. |
--mv-word-rotate-duration | Default duration. | |
--mv-word-rotate-stagger | Default stagger. | |
--mv-word-rotate-easing | Letter easing curve. | |
--mv-word-rotate-width-easing | var(--mv-ease-emphasized) | Width glide easing curve. |
--mv-word-rotate-radius | 0.3em | Pill corner radius. |
Accessibility
The animated word is aria-hidden; a visually hidden sentence lists every option once (“designers, developers, or teams”, built with Intl.ListFormat in the page language), so screen readers never hear a change. There is no live region. The rotation holds while the pointer is on the word or while focus is inside a wrapping link or button, pauses off-screen and in hidden tabs, and under reduced motion shows one static word with no width animation. The pill keeps a visible outline in forced colors.