Skip to content

Spinning Text

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.

Motionv0.1.0experimentalWCAG 2.2 AAView spec

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.

design system • open source • accessible •
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-text

Copies 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.json

Works, 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

new release • read the notes •
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

slow down • breathe in • breathe out •
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.

PropTypeDefault
childrenrequired

The text to set on the circle. A trailing separator such as a dot reads well when the text is shorter than the ring.

stringnone
radius

Radius of the circle in pixels, measured to the middle of the letters.

number72
duration

Seconds for one full turn.

number20
reverse

Turns the other way.

booleanfalse
isPaused

Stops the animation and shows the finished text. Moving content that lasts more than five seconds should be stoppable.

booleanfalse

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