beta
Circular Text <mv-circular-text>
Spinning ring of text around an icon, logo or avatar, drawn along a real SVG circle so the font keeps its kerning and the loop closes evenly; hover or focus eases the spin faster, slower, to a stop or into reverse. Screen readers hear the text once, and the ring holds still under reduced motion.
| Category | Animated text |
|---|---|
| Type | Web Component (<mv-circular-text>) |
| Status | beta |
| Keywords | circular, spinning-text, badge, sticker, scroll-indicator, rotate, svg, accessible |
When to use
- A round badge or sticker needs text spinning around a logo, avatar or icon
- A hero corner needs a looping indicator such as 'scroll down' or 'available for work'
- A spinning link badge should pause while it is hovered or focused so it can be read
Avoid when
- The text is long or must be read quickly; ring text is hard to scan beyond a few words
- Several words should replace each other inline inside a sentence → use Word Rotate instead
Install
node scripts/add.mjs circular-text --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["circular-text"], 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/circular-text/circular-text.js, components/circular-text/circular-text.css.
Usage
Canonical markup, to start from and customize with attributes, data-* and CSS variables:
<div style="display:flex;flex-wrap:wrap;gap:2.5rem;align-items:center;justify-content:center">
<a href="#" aria-label="Scroll to explore" style="color:inherit;text-decoration:none;border-radius:50%">
<mv-circular-text text="Scroll to explore • " hover="pause" duration="16000" style="font:600 .85rem/1 var(--mv-font-mono);text-transform:uppercase;letter-spacing:.08em">
<span data-center style="display:grid;place-items:center;width:3rem;height:3rem;border-radius:50%;background:var(--mv-fg);color:var(--mv-bg)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M6 13l6 6 6-6"/></svg>
</span>
</mv-circular-text>
</a>
<mv-circular-text text="New • 2026 Edition • " repeat="2" hover="reverse" duration="12000" style="font:700 .95rem/1 var(--mv-font-display);text-transform:uppercase;letter-spacing:.12em;color:var(--mv-fg-on-accent);background:radial-gradient(circle at 30% 25%,color-mix(in oklch,var(--mv-accent),white 18%),var(--mv-accent) 60%);box-shadow:var(--mv-shadow-lg), inset 0 0 0 1px color-mix(in oklch, white 20%, transparent)">
<svg data-center width="34" height="34" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.5l2.6 6.1 6.6.6-5 4.4 1.5 6.5L12 16.7l-5.7 3.4 1.5-6.5-5-4.4 6.6-.6z"/></svg>
</mv-circular-text>
<mv-circular-text text="Available for new projects • " hover="slow-down" direction="ccw" duration="22000" style="font:600 .8rem/1 var(--mv-font-sans);letter-spacing:.16em;text-transform:uppercase;color:var(--mv-fg-muted)">
<span data-center aria-hidden="true" style="display:grid;place-items:center;width:3.6rem;height:3.6rem;border-radius:50%;background:var(--mv-bg-muted);border:1px solid var(--mv-border);font:700 1.1rem/1 var(--mv-font-display);color:var(--mv-fg);position:relative">
AK
<span style="position:absolute;right:.1rem;bottom:.15rem;width:.7rem;height:.7rem;border-radius:50%;background:var(--mv-success);box-shadow:0 0 0 2px var(--mv-bg)"></span>
</span>
</mv-circular-text>
</div>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
text | string | Ring text (otherwise the child text). A trailing separator (“ • ”) makes a clean loop; a space is added when there is none. | |
label | string | Replaces the screen reader text. By default it is the ring text, read once, with separators turned into commas. | |
repeat | number | 1 | Repeats the text around the ring (up to 12 times); screen readers still hear it once. |
duration | number (ms) | 20000 | Duration of one full turn. |
direction | cw | ccw | cw | Rotation direction. |
hover | speed-up | slow-down | pause | reverse | none | speed-up | Reaction to mouse hover and to keyboard focus, on the ring or on a wrapping link or button; the speed eases on a spring. Taps do not count as hover. |
paused | boolean | Stops the rotation. It also stops by itself off-screen, in hidden tabs and under reduced motion. |
Content structure
| Name | Description |
|---|---|
[data-center] | Centered element (icon, avatar, logo). The ring grows so it never overlaps it. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-circular-text-radius | (auto, from the measured text) | Forced ring radius (to the middle of the letters); the spacing stretches or tightens to fill it. |
--mv-circular-text-spread | 1.05 | Spacing factor when the radius is automatic (1 = the font's natural spacing, 1.2 = 20% looser). |
--mv-circular-text-advance | 0.62em | Average width per character, used only while the text cannot be measured yet (hidden or not rendered). |
Accessibility
The ring is an aria-hidden SVG; a visually hidden copy gives screen readers the text once as a normal string, without repeats, with bullet separators read as commas (or the label attribute). Hover with a mouse or pen and keyboard focus (inside the badge or on a wrapping link or button) apply the hover reaction, so hover="pause" holds the text still to be read. The rotation also stops off-screen, in hidden tabs and under reduced motion, where the ring stays static and readable. The letters use currentColor, so forced colors apply. When the badge sits inside a link, give the link an explicit aria-label.