Letter Swap <mv-letter-swap>
Roulement lettre par lettre pour les liens de navigation et les boutons : au survol ou au focus clavier, chaque lettre roule pour révéler son double, en cascade, ou tourne comme un petit cube 3D. Les vraies lettres restent dans le DOM et dans leurs propres boîtes fixes, si bien que le libellé ne se recompose jamais, passe à la ligne entre les mots, reste sélectionnable et peut changer à l’exécution.
| Catégorie | Texte animé |
|---|---|
| Type | Web Component (<mv-letter-swap>) |
| Statut | stable |
| Keywords | letter, swap, roll, flip, 3d, hover, nav, button, stagger, accessible |
When to use
- Nav links or button labels should roll their letters on hover and keyboard focus
- A 3D cube flip of each letter suits a playful or technical brand
- A short label should roll once when it enters the viewport, on page load or on demand
- The revealed letters should take an accent color while a control is hovered
Avoid when
- A different label must roll in on hover, or a highlighter should glide between phrases → use Rolling Text instead
- Long sentences or paragraphs; the per-letter roll is meant for short labels
- The label is underlined; per-letter boxes do not carry the parent text decoration
Installation
node scripts/add.mjs letter-swap --out ./src/marvelousAgent IA avec le serveur MCP de Marvelous UI : install_components({ slugs: ["letter-swap"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Fichiers copiés (dépendances comprises) : tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/letter-swap/letter-swap.js, components/letter-swap/letter-swap.css.
Utilisation
Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :
<div style="display:grid;gap:2.25rem;justify-items:center;text-align:center">
<nav aria-label="Demo" style="display:flex;gap:.25rem;padding:.35rem;border:1px solid var(--mv-border);border-radius:999px;background:var(--mv-surface);box-shadow:var(--mv-shadow-sm)">
<a href="#" style="padding:.45rem .9rem;border-radius:999px;color:var(--mv-fg);text-decoration:none;font:500 .9rem var(--mv-font-sans)"><mv-letter-swap>Work</mv-letter-swap></a>
<a href="#" style="padding:.45rem .9rem;border-radius:999px;color:var(--mv-fg);text-decoration:none;font:500 .9rem var(--mv-font-sans)"><mv-letter-swap stagger-from="center">Studio</mv-letter-swap></a>
<a href="#" style="padding:.45rem .9rem;border-radius:999px;color:var(--mv-fg);text-decoration:none;font:500 .9rem var(--mv-font-sans)"><mv-letter-swap stagger-from="random">Journal</mv-letter-swap></a>
<a href="#" style="padding:.45rem .9rem;border-radius:999px;background:var(--mv-fg);color:var(--mv-bg);text-decoration:none;font:500 .9rem var(--mv-font-sans)"><mv-letter-swap direction="down">Contact</mv-letter-swap></a>
</nav>
<p style="margin:0;font:800 clamp(2.2rem,6vw,3.8rem)/1 var(--mv-font-sans);letter-spacing:-.045em;text-transform:uppercase">
<mv-letter-swap variant="flip" trigger="view" loop stagger="45" duration="700">Creative</mv-letter-swap>
<span style="color:var(--mv-fg-muted)">by nature</span>
</p>
<div style="display:flex;gap:.75rem;flex-wrap:wrap;justify-content:center">
<button class="mv-button" data-size="lg"><mv-letter-swap variant="flip" stagger="30">Start for free</mv-letter-swap></button>
<button class="mv-button" data-size="lg" data-variant="outline"><mv-letter-swap stagger="15" duration="420" easing="out" style="--mv-letter-swap-twin:var(--mv-accent)">Watch the demo</mv-letter-swap></button>
</div>
</div>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
text | string | Label to roll. Defaults to the element text; changing it rebuilds the letters. | |
variant | slide | flip | slide | slide: the letter slides; flip: a rotating 3D cube. |
trigger | hover | view | load | manual | hover | hover follows the parent link or button (mouse or pen hover, keyboard focus); view and load roll once; manual waits for replay(). |
direction | up | down | up | Roll direction: up (the twin rises from below) or down. |
duration | number | 500 | Duration per letter (ms). |
stagger | number | 22 | Delay between letters (ms). |
stagger-from | start | center | end | random | start | Where the cascade starts. |
delay | number | 0 | Initial delay (ms). |
easing | CSS easing | var(--mv-ease-in-out) | Easing curve: any CSS easing, or a token name (out, in, in-out, emphasized, spring, bounce, snappy). |
loop | boolean | With view, load or manual: keeps rolling while visible, pausing 1.6 s between rolls. |
Properties
| Name | Type | Description |
|---|---|---|
label | string | Read only. The label as announced to screen readers. |
Methods
| Name | Description |
|---|---|
replay() | Rolls every letter once and settles back invisibly. Returns a Promise resolved when done (at once under reduced motion). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-letter-swap-leading | 1.2 | Unitless line height of each letter box (the clipping window). The extra leading is cancelled, so the line keeps its height. |
--mv-letter-swap-easing | var(--mv-ease-in-out) | Default easing curve. |
--mv-letter-swap-twin | currentColor | Color of the revealed twin while hovered or focused (a replay always lands on identical letters). |
--mv-letter-swap-perspective | 20em | Perspective depth (flip variant). |
Accessibility
Screen readers get the label once, as plain text; the rolling letters are aria-hidden, so there is no per-letter reading. The real letters stay in the DOM (selectable, copied once) and the twin is drawn by CSS. Hover follows the parent control and its keyboard :focus-visible; touch does not trigger it. Each letter keeps a fixed box, so nothing reflows. Reduced motion, print and forced colors show static text.