All components
Shimmer
A GPU-accelerated highlight sweep over inline text for streaming and busy labels.
Generating response…Searching the docs…
12345678910111213141516import { Shimmer } from "#components/ui/ark-ui/shimmer.tsx";
export function Example() { return ( // The highlight is masked to the glyphs and only animates a transform, // so the sweep runs on the GPU in Chrome/Safari; Firefox falls back to // -moz-element() masking. Reduced motion gets the plain muted text. <> <Shimmer>Generating response…</Shimmer> <Shimmer duration="1.2s" spread="2ch">Searching the docs…</Shimmer> </> );}
// the highlight color is a custom property, overridable per instance:// <Shimmer style={styles.accent}> with { "--shimmer-color": "..." }Install
1npx stylexblocks add shimmerSource
components/ui/ark-ui/shimmer.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124import * as stylex from "@stylexjs/stylex";import { useId } from "react";import type { StyleOverrideProps } from "#lib/recipe-props.ts";import { semanticColors } from "@stylexblocks/ui/semantic-tokens/colors.stylex.ts";
const shimmerSweep = stylex.keyframes({ from: { translate: "-100% 0" }, to: { translate: "100% 0" },});
const styles = stylex.create({ root: { "--shimmer-color": semanticColors.fg, "--shimmer-duration": "2.5s", "--shimmer-spread": "4ch", color: semanticColors.fgMuted, display: "inline-block", position: "relative", }, text: { display: "inline-block", }, // Inert clone stacked over the original text; `-webkit-mask-clip: text` // clips the ::after sweep to the clone's glyphs (Chrome/Safari — the whole // overlay stays GPU-composited since the sweep only animates `translate`). // Masking a clone instead of the original keeps the visible text's // sub-pixel antialiasing intact. Firefox lacks the text mask and instead // masks with `-moz-element()` pointed at the visible text span (the // `--shimmer-moz-mask` var set per instance) — visually identical, though // element masking is not GPU accelerated. Under prefers-reduced-motion, or // without either mask, the overlay is hidden and the text renders plainly. overlay: { color: "transparent", contain: "strict", display: { default: "none", "@supports ((-webkit-mask-clip: text) or (mask-image: -moz-element(#t)))": { default: "block", "@media (prefers-reduced-motion: reduce)": "none", }, }, inset: 0, maskImage: { default: "linear-gradient(#fff, #fff)", "@supports (mask-image: -moz-element(#t)) and (not (-webkit-mask-clip: text))": "var(--shimmer-moz-mask)", }, overflow: "clip", pointerEvents: "none", position: "absolute", userSelect: "none", WebkitMaskClip: "text", "::after": { animationDuration: "var(--shimmer-duration)", animationIterationCount: "infinite", animationName: shimmerSweep, backgroundImage: "linear-gradient(90deg, transparent calc(50% - var(--shimmer-spread)), var(--shimmer-color) 50%, transparent calc(50% + var(--shimmer-spread)))", content: "''", inset: 0, position: "absolute", willChange: "transform", }, },});
const dynamic = stylex.create({ mozMask: (mask: string) => ({ "--shimmer-moz-mask": mask }), duration: (duration: string) => ({ "--shimmer-duration": duration }), spread: (spread: string) => ({ "--shimmer-spread": spread }),});
export interface ShimmerProps extends Omit<React.ComponentProps<"span">, "style">, StyleOverrideProps { /** One full sweep's pace as a CSS time, e.g. `"1.2s"`. Default `"2.5s"`. */ duration?: string; /** Half-width of the highlight band, e.g. `"2ch"`. Default `"4ch"`. */ spread?: string;}
/** * Shimmer — a GPU-accelerated highlight sweep over inline text, for * streaming/busy labels ("Generating response…"). Children render twice: the * visible text plus an aria-hidden clone that masks the animated gradient to * its glyphs, so only a cheap transform animates each frame in Chrome and * Safari; Firefox falls back to `-moz-element()` masking (same look, CPU * composited). Pace and band width are the `duration` / `spread` props; the * highlight color overrides through the `--shimmer-color` custom property. * Reduced motion gets the plain text. */export function Shimmer(allProps: ShimmerProps) { const { palette, style, className, children, duration, spread, ...rest } = allProps; // -moz-element() needs an id selector; strip useId's wrapper punctuation const textId = `shimmer-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`; const sx = stylex.props( palette, styles.root, dynamic.mozMask(`-moz-element(#${textId})`), duration != null && dynamic.duration(duration), spread != null && dynamic.spread(spread), style, ); return ( <span {...rest} className={ typeof className === "string" && className.length > 0 ? [sx.className, className].filter(Boolean).join(" ") : sx.className } style={sx.style} > <span id={textId} {...stylex.props(styles.text)}> {children} </span> <span aria-hidden="true" {...stylex.props(styles.overlay)}> {children} </span> </span> );}