Skip to content

Morphing Text

A phrase that blurs and cross-fades into the next phrase in a list, in the same spot. The slot is as wide as the longest phrase, so nothing around it jumps.

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.

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

export default function MorphingTextBasicExample() {
  return (
    <div className="flex w-full justify-center text-3xl font-semibold">
      <MorphingText texts={["Design", "Build", "Ship", "Repeat"]} />
    </div>
  );
}

Installation

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

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

Usage

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

<div className="flex w-full justify-center text-3xl font-semibold">
  <MorphingText texts={["Design", "Build", "Ship", "Repeat"]} />
</div>

Slow and large

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

// A longer interval and a slower morph suit a large hero line.
export default function MorphingTextSlowAndLargeExample() {
  return (
    <div className="flex w-full justify-center text-5xl font-bold">
      <MorphingText texts={["Fewer clicks", "Faster pages", "Calmer teams"]} interval={4000} duration={1400} />
    </div>
  );
}

Paused

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

// Anything that keeps moving should be stoppable: isPaused holds the current phrase.
export default function MorphingTextPausedExample() {
  const [paused, setPaused] = useState(false);
  return (
    <div className="flex w-full flex-col items-center gap-4">
      <div className="text-3xl font-semibold">
        <MorphingText texts={["Plan", "Draft", "Review", "Publish"]} isPaused={paused} />
      </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
textsrequired

The phrases to morph through, in order.

string[]none
interval

Milliseconds each phrase stays before the next one replaces it.

number3000
duration

Milliseconds the blur and cross-fade take.

number900
label

Accessible name for the whole slot. Defaults to all the phrases, joined with commas.

stringnone
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 list of all phrases, once

What your code must do

  • The slot has an accessible name listing every phrase once, and the visible layers are hidden from assistive technology
  • Hidden sizing copies of the other phrases are invisible to everyone, so nothing is read twice
  • 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 hero line that cycles through a few short statements
  • Showing a range of things a product handles

Avoid it when

  • Phrases that carry information people must not miss
  • Lists longer than six phrases

Don't

  • Putting a different meaning in each phrase and expecting every one to be read
  • Very short intervals, which make the text hard to read

Design tokens

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

  • --rd-color-text-default