Skip to content

Shimmer

Use a shimmer when the text on screen is the status — "Generating response…", "Thinking", a title still being written — and a Spinner beside it would say the same thing twice. It is not a placeholder: a Skeleton stands in for content that has not arrived, while a shimmer plays over words that already have. If the wait has a measurable end, show a Progress bar instead.

shimmer/basic.tsx
Generating response…Thinking about your question

The text stays real text. It is selectable, a screen reader reads it as written, and it keeps whatever color it inherited — only the paint changes, and only while active. The gradient is clipped to the glyphs, so nothing is drawn outside the letters and nothing sits on top of them.

Import

import { Shimmer } from "@forte-ui/react";

Examples

Turning it off

active is a flag, not a mount. The same <span> renders in both states, so a label can go from working to done without anything around it moving.

shimmer/active.tsx
Summarizing thread…

Speed

Speed is one knob, --forte-shimmer-duration: the length of one cycle, from the band entering one edge of the text to leaving the other. It defaults to --forte-duration-loop-sweep (1400ms) and is set in a style object, the way every component's knobs are.

shimmer/speed.tsx
Generating response…700ms
Generating response…1400ms (default)
Generating response…3s

Once

A single pass says look here, where a loop says still waiting. The band holds its last frame off the far edge, so the text ends up plain.

shimmer/once.tsx
Changes saved

Direction

The sweep follows the reading direction, multiplying its travel by --forte-direction, so it runs right-to-left in RTL without being told. Flip the frame above to RTL to see it. reverse runs against the reading direction in either mode.

shimmer/direction.tsx
Follows the reading directionRuns against it

Reduced motion

shimmer/reduced-motion.tsx
Full motionGenerating response…The band sweeps
prefers-reduced-motionGenerating response…The band is parked and hidden

Under reduced motion the band does not travel, and it is not drawn: the travel is gated on --forte-motion-ok, and the band's color is mixed back into currentColor by --forte-motion-off, so what is left is the text in its own color. There is deliberately no fallback pulse, and that is a departure from Skeleton, whose shimmer degrades into a breathe.

The two differ because of what the animation is playing over. A skeleton that stops moving reads as content that failed, so it has to keep some cue alive. Here the text is the content, and it already carries the message in words. Dimming "Generating…" on a loop would spend the reader's contrast to say what the word says — the wrong trade for someone who has asked for less.

Theming

shimmer/theming.tsx
Accent bandThe quick brown fox jumps over the lazy dog
Narrow and fastThe quick brown fox jumps over the lazy dog
Wide, upright and slowThe quick brown fox jumps over the lazy dog

Accessibility

There is nothing to announce and nothing to hide. The shimmer is a paint effect on text that would be on the page anyway, so the text is in the accessibility tree exactly as written, and the effect is not. That is the reason it does no role="status" of its own: if "Generating response…" should be announced when it appears, the live region belongs to the container that swaps the message in, the same way the Spinner page recommends — and it should not be announced twice.

Under forced colors the fill is put back and the gradient dropped. Forced colors remaps color to CanvasText but does not remap background-image or the transparent fill the clip relies on, so left alone the glyphs would be painted in our gradient — a gray band in a theme whose entire promise is that the page uses the user's colors. The text is the message, and it is still there.

It is a <span>, display: inline, so it wraps with the sentence it sits in. It is not in the tab order and has no cursor of its own.

Theming

Everything below is declared on the shimmer itself, which is also why an ancestor is the wrong place to set one — the element's own declaration beats an inherited value. Override them through className or a style object.

Theming tokens for Shimmer
PropertyControlsDefault
--forte-shimmer-colorColor of the band. Defaults to the text's own color, faded in light mode and lifted toward white in dark mode.light-dark( oklch(from currentColor l c h / calc(alpha * 0.2)), oklch(from currentColor max(0.8, calc(l + 0.4)) c h / calc(alpha + 0.4)) )
--forte-shimmer-durationOne full cycle. Speed is this one knob: shorter is faster.var(--forte-duration-loop-sweep)
--forte-shimmer-spreadHalf-width of the band, measured across the text. Part ch so it scales with the type and part a fixed run so a small label still gets a visible gleam; a length rather than a fraction of the text, so a one-word label and a full sentence get the same light.calc(3ch + var(--forte-space-7))
--forte-shimmer-angleTilt of the band off vertical. 0deg is a straight edge.20deg

--forte-shimmer-color is not a plain semantic reference, and the reason is worth knowing. "A light passing over the ink" is drawn differently against a light page and a dark one, so the default is a light-dark() pair, each branch derived from currentColor. In light mode the band is the text at a fifth of its alpha: the page shows through the glyphs, which reads as a gleam without needing a color lighter than the ink. In dark mode a faded band would read as a dip, so the band is lifted toward white and made more opaque instead. Either way the same shimmer sits in muted prose, in a solid button and on an inverted tooltip without a tone prop.

--forte-shimmer-spread is the band's half-width, and it is a length rather than a fraction of the text, so a one-word label and a whole sentence get the same band instead of one the width of the line. It is part ch so it scales with the type and part a fixed run so a small label still gets a visible gleam.

State is on data-active, data-once and data-reverse, all reachable from plain CSS or a Tailwind arbitrary variant — data-[active]:... — without a wrapper element.

API reference

There is no Base UI counterpart to forward to. Shimmer is a <span> and takes that element's whole surface — id, style, data-*, ref — but no render prop.

Props for Shimmer
PropTypeDefaultDescription
activebooleantrueWhether the highlight is sweeping. `false` renders the children as ordinary text — same element, same layout, nothing else changes — so a label can go from "Generating…" to its finished state by flipping one flag rather than swapping components.
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
oncebooleanfalseSweep once and stop, instead of looping. For a line that has just arrived — a new message, a freshly saved title — where a single pass says "look here" and a loop would say "still waiting".
reversebooleanfalseSweep against the reading direction. The default follows `dir`: left to right in LTR, right to left in RTL, without being told.