Components / Shimmer

Shimmer

Text that represents live, streaming, or thinking work: a "Thinking" line in chat, a status label while an agent runs. The sweep says "in motion" without a spinner.

@import 'shadcn/tailwind.css'  /* the shimmer @utility ships with shadcn@4.13; every consumer stylesheet already imports it */

Rendered stock

House rules

  • Shimmer is a CSS utility class, not a component: put the shimmer class on the text element itself.
  • The standard shadcn utility is the ONE shimmer; bespoke shimmer implementations were deleted and must not return. This has now happened twice — the second time by an agent who grepped the repo for a `.shimmer` rule, found none because the utility ships inside `shadcn/tailwind.css`, and concluded it was undefined. It is defined. Check the stock stylesheet before concluding a utility is missing.
  • Call sites write `shimmer text-muted-foreground` and NOTHING else. The house tuning is set once, centrally, in `packages/ui/src/css/tailwind.css` through the utility's own `--shimmer-color` / `--shimmer-spread` knobs. A per-call-site recipe is how this last went wrong: the deleted SHIMMER_TUNING constant carried `shimmer-color-foreground`, which matched the element's own text color and flattened the gradient to nothing in BOTH themes.
  • THE BAND DIPS TOWARD THE SURFACE, in both themes (founder, 2026-08-04, matching ChatGPT's thinking line). Light mode darkens toward white, dark mode darkens toward the dark surface — the two themes are inverses of each other, and the sweep always reads as a band travelling THROUGH the text rather than a flare sitting on top of it.
  • This overrides stock, deliberately. Shadcn's dark branch BRIGHTENS (`max(0.8, l + 0.4)`) instead of dipping. Beyond reading as the opposite of light mode, it has no headroom on a light base: Strata's dark `--foreground` is pure white, so base and highlight both resolved to `#fff` and the sweep sat perfectly still. That is how shimmer shipped invisible in dark mode at every call site in the app.
  • The house spread is `2.5ch`, down from the stock `3ch + 40px`. Stock is WIDER than a short label — on "Thinking" the whole word sits inside the band and pulses in place instead of a band crossing it. Most live status copy here is one or two words.
  • The house tempo is 1.7s (founder, 2026-08-04), and the utility is capped at `max-width: fit-content` so that duration MEANS something. The sweep animates `background-position` across the ELEMENT BOX, not the text, so a shimmer blockified by a flex column stretched to the full column width: a 69px "Thinking…" sat in a 718px box, the band crossed the word in a fifth of a second, and the rest of the cycle was dead air — while the identical class on a flex-ROW child swept smoothly across the whole duration. Same duration, visibly different motion. The cap collapses a stretched box onto its text without disturbing a wrapping paragraph or a `truncate`d label.
  • The remaining knobs (`shimmer-duration-*`, `shimmer-angle-*`, `shimmer-once`, `shimmer-reverse`, `shimmer-none`) exist, but reach for them only with a reason you can state. Anything you want from a shimmer is one of these — never a fork.
  • Shimmer marks in-flight text only; settled copy never shimmers. Where a slot switches between the two (Attachment's title), move the color with the state so the settled copy keeps its `foreground` ink.

See also