Skip to content
txbrendan/ui

Shimmer

beta

Loading text with a slanted band of light that sweeps across it, like "Thinking" while an answer streams in. It can also cycle through messages.

Preview

gloss

ThinkingSearching the webWriting your report

glint

ThinkingSearching the webWriting your report

color

ThinkingSearching the webWriting your report

text

ThinkingThinkingThinkingWriting the answer

icon

Thinking

messages

Thinking

no loop

Thinking

beside

Thinking

reply

How do I rotate my API key?

Searching the docs

on card

Building your report

dark section

ThinkingSearching the web

motion off

ThinkingThinking

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/shimmer

Use it for

  • A short line of text that says work is happening, like "Thinking" or "Writing the answer" while a reply streams in.
  • Real progress steps. Pass the current step as children and change it as the work moves on.
  • Put it inside a role="status" element that stays on the page, so screen readers hear each step once.
  • shine picks the light: gloss, a soft sheen with a bright streak, glint, two streaks like light on glass, or color, a band that runs through the theme's tone colors.
  • ShimmerStack for a few lines at once, like loading steps. It starts each line a little after the one above, so the light runs down them in order, even when the lines differ in length or size.
  • ShimmerMessages for filler text that does not report real progress, like "Thinking", "Still thinking", "Almost there". It shows each message for interval milliseconds.
  • It takes the font size and weight around it. Set them with className, from the theme's type sizes, like text-(length:--type-body) font-(--type-weight-strong).

Avoid it for

  • Content that is loading in a known shape, like a card or a list. Use Skeleton.
  • An action in progress, like saving. Use loading on the Button.
  • Solid tone fills, like a brand button. The band reads the card color, so it has no room to show there.
  • ShimmerMessages for real steps. Screen readers hear only its first message. Use Shimmer and change its children instead.
  • Long paragraphs. Keep it to one short line. If it wraps, each line gets its own band.
  • Emoji. The band fills the letters with the text color, so an emoji turns into a flat shape.

Props

  • children

    Shimmer only. The text that shimmers. Required. An icon inside keeps the text's color.

    ReactNode

  • shine

    How the light looks. gloss is a soft sheen with a bright streak. glint is two streaks, a small one and a bright one. color runs through the theme's tone colors.

    "gloss" | "glint" | "color" · default "gloss"

  • ShimmerStack

    A column for stacked Shimmer lines. Each child starts its sweep 120ms after the one above, up to the seventh, and every line's band moves at the same speed. A ShimmerMessages line keeps its place in the ripple as its message changes. Set [--shimmer-stagger:200ms] to change the step.

    component

  • messages

    ShimmerMessages only. The messages to show in turn. Screen readers hear only the first one.

    readonly string[]

  • interval

    ShimmerMessages only. How long each message shows, in milliseconds.

    number · default 2000

  • loop

    ShimmerMessages only. Start over after the last message. Set false to stop on the last one.

    boolean · default true

  • className

    Merged last. 'animate-none' stops the sweep. '[animation-delay:120ms]' starts it later. '[--shimmer-band:32ch]' makes the band wider.

    string

Accessibility

  • Shimmer is real text, so screen readers read it and people can select it. Selected text shows solid, with no band. The sweep is only a look.
  • It is not a live region. To have screen readers hear it, keep one role="status" element on the page and put the Shimmer inside it while work runs.
  • Keep that status element on the page before the text arrives. Some screen readers miss a status region that arrives together with its text.
  • ShimmerMessages gives screen readers its first message only, and hides the rest from them. A status region around it then speaks once, not on every change.
  • At rest the text is the foreground color in a light theme, and a little dimmer in a dark one, so the band can brighten it. The band passes over a few letters at a time and fades them for a moment. That fade has no contrast check, so never put the only copy of something people must read in a Shimmer.
  • The sweep runs over 1.3 times --motion-pulse-duration. The theme sets that to 0s for motion off and for reduced motion, so the text then shows plain, with no band.
  • In forced colors the band is dropped and the text takes the system text color.
  • In ShimmerMessages, each new message fades in from --motion-distance below while the last one fades out. Reduced motion keeps the fade and drops the movement. Motion off swaps the text at once.
  • ShimmerMessages holds its width at the longest message, so content beside it does not shift when the message changes.
  • In right-to-left text, the band sweeps from right to left.

Examples

Thinking

<Shimmer>Thinking</Shimmer>

Each shine

<Shimmer shine="gloss">Thinking</Shimmer>
<Shimmer shine="glint">Searching the web</Shimmer>
<Shimmer shine="color">Writing your report</Shimmer>

Stacked lines with the light rippling down

<ShimmerStack>
  <Shimmer>Thinking</Shimmer>
  <Shimmer>Searching the web</Shimmer>
  <Shimmer className="text-(length:--type-section)">Writing your report</Shimmer>
</ShimmerStack>

Real steps in a status region that stays on the page

function Reply({ step }: { step: string | null }) {
  return (
    <p role="status">
      {step === null ? null : <Shimmer>{step}</Shimmer>}
    </p>
  );
}

Cycling filler messages

<ShimmerMessages messages={["Thinking", "Still thinking", "Almost there"]} interval={2500} loop={false} />

With an icon (needs lucide-react)

import { Sparkles } from "lucide-react";

<Shimmer className="inline-flex items-center gap-1.5">
  <Sparkles className="size-4" aria-hidden />
  Thinking
</Shimmer>