v1.5

Text Shimmer

Animated highlight that sweeps across a text label to show thinking, streaming or loading in progress.

Pro
Searching the docs for rate limits…

Description

TextShimmer renders a single <span> whose text carries a soft highlight band that sweeps from start to end on a loop. It is a single-element component: children render once, and the gradient is painted behind the glyphs with background-clip: text, so ids, state and effects inside the label are never duplicated.

Use it for short in-progress labels in AI and async UI: an assistant's "Thinking…" line, the trigger of a tool call row, a "Searching 12 sources…" status under a composer, or a label inside a busy button or badge. Flip active to false when the work is done and the same element settles into plain text.

Don't use it to indicate loading without text. For a dots indicator or placeholder skeleton use Chat Loader, and for a busy control use Spinner. Don't shimmer long answers or paragraphs either; shimmer the status line above them.

TextShimmer is a Create UI Pro component. Install it with a Pro seat.

Installation

pnpm dlx @create-ui/cli add text-shimmer

Usage

import { TextShimmer } from "@/components/ui/text-shimmer"
<TextShimmer>Thinking…</TextShimmer>

Examples

Every example is a Pro preview (marked with a Pro badge).

Color

The shimmer is built from currentColor, so any text-* color class on the root or a parent sets it. Outside the band the text keeps 80% of that color, which keeps text-body above 4.5:1 on the default surfaces.

Pro
Thinking…Drafting a reply…Generating image…Running tests…Retrying request…

Typography

There is no size prop. The band width is measured in ch, so it scales with whatever type class you apply, from text-ui-caption-xs up to headings.

Pro
Reading 14 files…Reading 14 files…Reading 14 files…Reading 14 files…Reading 14 files…

Spread

--spread sets the half-width of the highlight band (default 4ch). Narrow it with [--spread:2ch] for a sharp glint, or widen it with [--spread:12ch] for a slow wash across long labels.

Pro
Narrow band: comparing pricing tiers…Default band: comparing pricing tiers…Wide band: comparing pricing tiers…

Duration

--duration is the length of one sweep (default 2.5s). The band crosses the whole label once per sweep, so a long label moves faster than a short one. Raise it with [--duration:5s] for long lines and lower it for one-word labels.

Pro
Transcribing…Transcribing the meeting recording…Summarizing a 42-page contract and pulling out renewal dates…

With icon

The root is inline-block. Pass inline-flex items-center gap-2 to lay out an icon and label inside it. Only text glyphs shimmer, so icons stay solid. To keep a spinner or icon outside the effect, put it next to the TextShimmer instead of inside.

Pro
Generating response…Searching 12 sources…
Reading app/layout.tsx

Active

active defaults to true. Set it to false when the work finishes and the same span renders plain text, with nothing remounted. Keep one tree and flip the prop rather than swapping TextShimmer for bare children. The role="status" wrapper announces the finished label.

Pro

Thinking…

Accessibility

TextShimmer is presentational text. It renders a <span>, is not focusable and has no keyboard interactions.

KeyDescription
-Not focusable by default.

ARIA notes:

  • The component sets no role and no aria-* attributes. Screen readers read the label once, as plain text.
  • It is not a live region. If the label changes as work progresses (for example "Thinking…" to "Thought for 3 seconds"), wrap it in an element with role="status" or aria-live="polite".
  • If the in-progress state needs to be exposed on a larger region, set aria-busy on that region yourself.
  • Decorative icons passed as children should carry aria-hidden="true".
  • Under prefers-reduced-motion: reduce the animation stops and the text renders at full color. Under forced-colors: active the gradient is dropped so system colors apply.

Styling

Tailwind override: pass className to set color, type and layout, and override the CSS variables through arbitrary properties:

<TextShimmer className="text-primary-base text-body-sm [--duration:4s] [--spread:6ch]">
  Generating image…
</TextShimmer>

While active, the root's background is clipped to its text, so put backgrounds and borders on a wrapper, not on TextShimmer itself. Nested text elements take the root's color while active.

Data slots and attributes: the component sets these for CSS targeting:

  • data-slot="text-shimmer" on the root <span>.
  • data-active="true" or data-active="false" on the root, mirroring the active prop.

It also reads two CSS custom properties from the root:

  • --spread: half-width of the highlight band. Default 4ch.
  • --duration: length of one sweep. Default 2.5s.

Target the finished state in CSS:

[data-slot="text-shimmer"][data-active="false"] {
  /* … */
}
  • Chat Loader: dots, spinner and skeleton indicators for when there is no label to show.
  • Spinner: a busy indicator for controls and rows; pair it with TextShimmer for a labelled status.
  • Chain Of Thought: a reasoning timeline whose trigger uses TextShimmer while it streams.
  • Chat Tool: tool call rows that shimmer their label while the call runs.

API Reference

TextShimmer

Animated text label. Extends React.ComponentProps<"span">, so any standard span attribute (id, role, aria-*, style, etc.) is accepted.

Props

PropTypeDefaultDescription
activebooleantrueRuns the shimmer. false renders the same span as plain text and sets data-active="false".
classNamestring-Tailwind classes merged via cn(). Set color, type, layout and --spread / --duration.
childrenReact.ReactNode-The label. Text and icons; only text glyphs shimmer.

Variants

TextShimmer has no CVA variants. Color and size come from the classes you apply, and the animation is tuned through CSS variables.

VariableOptionsDefaultDescription
--spreadany CSS length4chHalf-width of the highlight band.
--durationany CSS time2.5sLength of one sweep across the label.