Letter Swap <mv-letter-swap>
Rolagem letra a letra para links de navegação e botões: ao passar o mouse ou receber o foco do teclado, cada letra rola para longe e revela sua gêmea, em cascata, ou gira como um pequeno cubo 3D. As letras reais continuam no DOM e em suas próprias caixas fixas, então o rótulo nunca se reorganiza, quebra entre palavras, continua selecionável e pode mudar em tempo de execução.
| Categoria | Texto animado |
|---|---|
| Tipo | Web Component (<mv-letter-swap>) |
| Status | estável |
| 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
Instalação
node scripts/add.mjs letter-swap --out ./src/marvelousAgente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["letter-swap"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Arquivos copiados (dependências incluídas): 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.
Uso
Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis 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 | Tipo | 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 | Tipo | 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.