Text morph

Morph a label into its next state, letter by letter.

textmotionstatus
Preview
Text morph preview

Good interfaces are built on spacing.

When to use

  • Status words that change a few letters at a time, such as Follow and Following.
  • Custom buttons or badges that need a morphing label without the rest of the button.
  • Inline counters or short state labels that should glide instead of jump.

When not to use

  • Use word-rotate for a sentence with one cycling word.
  • Use button or action-swap when you want the morph plus button behaviour.
  • Avoid it for text that wraps, since it always renders a single line.

shadcn CLI

Also installs (1)

  • manicat-foundation
Manual install(+)
registry.jsonjson
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-morph",
  "type": "registry:ui",
  "title": "Text morph",
  "description": "Morph a label into its next state, letter by letter.",
  "categories": [
    "text"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://manicat.viraza.net/r/manicat-foundation.json"
  ],
  "files": [
    {
      "path": "registry/components/text-morph/text-morph.tsx",
      "type": "registry:component",
      "target": "@components/manicat/text-morph/text-morph.tsx"
    },
    {
      "path": "registry/components/text-morph/text-morph.module.css",
      "type": "registry:component",
      "target": "@components/manicat/text-morph/text-morph.module.css"
    }
  ],
  "docs": "Docs and live preview: https://uiarc.dev/components/text-morph",
  "meta": {
    "tier": "free",
    "kind": "component",
    "docs": "https://manicat.viraza.net/components/text-morph",
    "markdown": "https://uiarc.dev/components/text-morph/markdown",
    "tags": [
      "text",
      "motion",
      "status"
    ]
  }
}

Usage

Install the component, then copy this into your app. Change the content and the props to fit your screen.

example.tsxtsx
"use client";

import { useEffect, useState } from "react";
import { TextMorph } from "@/components/manicat/text-morph/text-morph";

const morphWords = ["spacing", "grid", "rhythm", "order"];

export function TextMorphExample() {
  const [index, setIndex] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setIndex((current) => (current + 1) % morphWords.length), 2400);
    return () => clearInterval(id);
  }, []);

  return (
    <p className="w-full text-center text-sm text-(color:--text-muted)">
      Good interfaces are built on <TextMorph as="strong">{morphWords[index]}</TextMorph>.
    </p>
  );
}

Examples

Real screens rather than isolated props. Switch to Code to copy a whole example into your project.

Rotating word A word inside a sentence that morphs into the next one, letter by letter.

Rotating word A word inside a sentence that morphs into the next one, letter by letter. preview

Good interfaces are built on spacing.

Accessibility

  • The plain label is in a visually hidden span; the animated glyphs are aria-hidden.
  • It does not announce changes. Wrap it in your own aria-live region if a status change must be announced.

Motion

  • Letters are matched by character and occurrence, so shared letters glide to their new positions on a spring while new letters stagger in with a blur.
  • The frame width springs to the new label; font loading or resizes follow immediately without animating.
  • Reduced motion swaps the label and width instantly.

Responsive behavior

  • The frame width springs to each label, and a ResizeObserver follows font loading and container resizes without animating.
  • The label never wraps, so keep it short in narrow layouts.

Performance

  • Every character is a motion span with layout animation; keep labels to a few words.

Notes for AI

  • Use for status words that change a few letters at a time (Follow / Following, Save / Saved). For a sentence with a cycling word use word-rotate.
  • Keep labels to one short line; it never wraps.