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.
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.
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.
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.
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.
Reduced motion
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
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.
| Property | Controls | Default |
|---|---|---|
--forte-shimmer-color | Color 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-duration | One full cycle. Speed is this one knob: shorter is faster. | var(--forte-duration-loop-sweep) |
--forte-shimmer-spread | Half-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-angle | Tilt 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.
| Prop | Type | Default | Description |
|---|---|---|---|
active | boolean | true | Whether 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. |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
once | boolean | false | Sweep 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". |
reverse | boolean | false | Sweep against the reading direction. The default follows `dir`: left to right in LTR, right to left in RTL, without being told. |