Text laid out round a circle that turns slowly. The layout is plain CSS (each letter is rotated about the centre), and the real text is in a visually hidden copy.
Optional, and kept apart.
Nothing here is included in a project unless you add it, and the plain component it builds on is unchanged. This effect is decoration: it is hidden from screen readers, stands still for visitors who prefer reduced motion, and can be paused. Use Still on a preview to see that version.
import { SpinningText } from "@rdloom/react";
export default function SpinningTextBasicExample() {
return (
<div className="flex w-full justify-center text-sm font-medium uppercase tracking-widest">
<SpinningText>design system • open source • accessible • </SpinningText>
</div>
);
}Installation
npx rdloom add spinning-textCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.
It also adds rdloom-motion.css next to your tokens and imports it from the tokens file, once: no setup, and no inline styles. The plain components you already use are untouched.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/spinning-text.jsonWorks, but without upgrade tracking, and you import the motion CSS yourself.
Usage
import { SpinningText } from "@rdloom/react";
<div className="flex w-full justify-center text-sm font-medium uppercase tracking-widest">
<SpinningText>design system • open source • accessible • </SpinningText>
</div>Around an icon
import { SpinningText } from "@rdloom/react";
// The ring is a plain box, so anything can sit in the middle of it.
export default function SpinningTextAroundAnIconExample() {
return (
<div className="flex w-full justify-center">
<div className="relative inline-flex items-center justify-center text-xs font-medium uppercase">
<SpinningText radius={56}>new release • read the notes • </SpinningText>
<span
aria-hidden="true"
className="absolute size-10 rounded-full"
style={{ background: "linear-gradient(135deg, var(--rd-color-action-primary), var(--rd-color-surface-subtle))" }}
/>
</div>
</div>
);
}Reverse and slow
import { useState } from "react";
import { Button, SpinningText } from "@rdloom/react";
// Turning the other way, slowly, with a switch to hold it still.
export default function SpinningTextReverseAndSlowExample() {
const [paused, setPaused] = useState(false);
return (
<div className="flex w-full flex-col items-center gap-3">
<div className="text-sm font-medium uppercase tracking-widest">
<SpinningText reverse duration={40} isPaused={paused} radius={64}>
{"slow down • breathe in • breathe out • "}
</SpinningText>
</div>
<Button variant="ghost" size="sm" onPress={() => setPaused((p) => !p)} aria-pressed={paused}>
{paused ? "Resume motion" : "Pause motion"}
</Button>
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
childrenrequiredThe text to set on the circle. A trailing separator such as a dot reads well when the text is shorter than the ring. | string | none |
radiusRadius of the circle in pixels, measured to the middle of the letters. | number | 72 |
durationSeconds for one full turn. | number | 20 |
reverseTurns the other way. | boolean | false |
isPausedStops the animation and shows the finished text. Moving content that lasts more than five seconds should be stoppable. | boolean | false |
Accessibility
Role group, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
Not interactive.
Screen readers announce
- The text, once
What your code must do
- The full text is in a visually hidden copy; the letters on the ring are hidden from assistive technology so nothing is read letter by letter
- The ring is a fixed square of 2 x radius plus the letter size, so the page does not shift as it turns
- With isPaused the ring stays at its resting angle and the text is still fully readable
- Decoration only: the animated layers are hidden from assistive technology and never take focus
- Stands still for visitors who ask for less motion (prefers-reduced-motion), and can also be shown still with the rdm-still class or isPaused
- In forced-colors (high contrast) mode the text is drawn in the system colours with no effect
- Does not flash: nothing changes more than three times in a second (WCAG 2.3.1)
- The text is real text in the server HTML, readable with no script
- Optional: it is a separate component, so projects that do not add it carry none of its code
Guidelines
Use it when
- A badge or seal round an icon or logo
- A decorative label that links to nothing
Avoid it when
- Long sentences: letters on a tight circle are hard to read
- Any text people must read quickly or act on
- Right-to-left or joined scripts, where cutting the text into letters breaks the shapes
Don't
- Using it as the only place a link or instruction appears
- A radius so small that letters overlap
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-text-default