Skip to content

Kinetic Text

Text whose letters move one after another in a loop: a wave, a bounce or a pulse. Plain CSS, staggered by letter position.

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.

Making waves
import { KineticText } from "@rdloom/react";

export default function KineticTextBasicExample() {
  return (
    <div className="flex w-full justify-center">
      <KineticText className="text-3xl font-bold">Making waves</KineticText>
    </div>
  );
}

Installation

npx rdloom add kinetic-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/kinetic-text.json

Works, but without upgrade tracking, and you import the motion CSS yourself.

Usage

import { KineticText } from "@rdloom/react";

<div className="flex w-full justify-center">
  <KineticText className="text-3xl font-bold">Making waves</KineticText>
</div>

Bounce

Boing boing
import { KineticText } from "@rdloom/react";

export default function KineticTextBounceExample() {
  return (
    <div className="flex w-full justify-center">
      <KineticText effect="bounce" stagger={0.1} className="text-3xl font-bold">
        Boing boing
      </KineticText>
    </div>
  );
}

Pulse and slow

Heartbeat
import { useState } from "react";
import { Button, KineticText } from "@rdloom/react";

// A slow pulse, with a switch to stop it.
export default function KineticTextPulseAndSlowExample() {
  const [paused, setPaused] = useState(false);
  return (
    <div className="flex w-full flex-col items-center gap-3">
      <KineticText effect="pulse" duration={3} stagger={0.15} isPaused={paused} className="text-2xl font-semibold">
        Heartbeat
      </KineticText>
      <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. Words never split across lines mid-word.

stringnone
effect

How each letter moves.

"wave" | "bounce" | "pulse""wave"
duration

Seconds for one loop of a single letter.

number1.6
stagger

Seconds between one letter starting and the next.

number0.08
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 animated letters are hidden from assistive technology
  • Letters move by a fraction of a line height and never leave their slot, so the text stays readable and does not reflow
  • 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 short playful heading or a loading message
  • A single word that should draw the eye

Avoid it when

  • Paragraphs and any text people have to read closely
  • Content that must stay still for people who are easily distracted: always offer isPaused

Don't

  • Looping a long sentence, which makes it tiring to read
  • Setting duration under one second

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-text-default