Installation
Run this in a project set up with shadcn/ui:
$ pnpm dlx shadcn@latest add https://uxelith.com/r/shimmer-text.jsonIt adds these packages for you:
clsxtailwind-mergeThe source lands in your components folder as
shimmer-text.tsx. It's yours: change anything.
Usage
import { ShimmerText } from "@/components/shimmer-text"
export function Announcement() {
return (
<ShimmerText as="h2" className="text-3xl font-semibold tracking-tight">
Introducing Studio
</ShimmerText>
)
}Source
One file, 53 lines, nothing hidden. Read it before you install it.
import * as React from "react";
import { cn } from "@/lib/utils";
export interface ShimmerTextProps extends React.HTMLAttributes<HTMLElement> {
/** Element to render, so headings stay headings. */
as?: "h1" | "h2" | "h3" | "h4" | "p" | "span";
/** Seconds for one sweep of the light. */
duration?: number;
/** Width of the band of light, in pixels. Defaults to a width that suits the length of the text. */
spread?: number;
/** Turn the shimmer off when the work is done; the text settles to its normal colour. */
active?: boolean;
}
/**
* Text with a band of light that sweeps across it — for "Thinking…" states,
* announcements and section labels.
*
* - The band is sized to the text, so a short label and a long heading
* sweep at the same pace; `active={false}` settles it when the work is done.
* - Real text: it selects, copies, translates and reads like any other.
* - Pure CSS (needs the `ux-shimmer` keyframes, which the registry adds), so
* it renders on the server. Colours: `--shimmer-base` (default
* `--muted-foreground`) and `--shimmer-light` (default `--foreground`).
* - Plain text under `prefers-reduced-motion` and in forced-colours mode.
*/
export function ShimmerText({ as: Tag = "span", duration = 2.5, spread, active = true, className, style, children, ...props }: ShimmerTextProps) {
const length = typeof children === "string" ? children.length : 20;
const band = spread ?? Math.min(Math.max(length * 5, 60), 240);
return (
<Tag
data-active={active || undefined}
className={cn(
"inline-block",
active
? [
"bg-clip-text bg-no-repeat text-transparent [background-size:250%_100%,auto]",
"bg-[linear-gradient(90deg,transparent_calc(50%-var(--shimmer-spread)),var(--shimmer-light,var(--foreground))_calc(50%-var(--shimmer-spread)*0.3),var(--shimmer-light,var(--foreground))_calc(50%+var(--shimmer-spread)*0.3),transparent_calc(50%+var(--shimmer-spread))),linear-gradient(var(--shimmer-base,var(--muted-foreground)),var(--shimmer-base,var(--muted-foreground)))]",
"animate-[ux-shimmer_var(--shimmer-duration)_linear_infinite]",
"motion-reduce:animate-none motion-reduce:bg-none motion-reduce:text-[var(--shimmer-light,var(--foreground))]",
"forced-colors:bg-none forced-colors:text-[CanvasText]",
]
: "text-[var(--shimmer-light,var(--foreground))]",
className,
)}
style={{ "--shimmer-duration": `${duration}s`, "--shimmer-spread": `${band}px`, ...style } as React.CSSProperties}
{...props}
>
{children}
</Tag>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| as | "h1" | "h2" | "h3" | "h4" | "p" | "span" | "span" | Element to render, so headings stay headings. |
| duration | number | 2.5 | Seconds for one sweep. |
| spread | number | from text length | Width of the band of light, in pixels. |
| active | boolean | true | Set false when the work is done; the text settles to its normal colour. |
| children | ReactNode | — | The text. |
| --shimmer-base | CSS variable | --muted-foreground | Text colour outside the light. |
| --shimmer-light | CSS variable | --foreground | Colour of the light. |
Accessibility
- Real text: it selects, copies, translates and is read like any other text.
- Plain, full-contrast text under prefers-reduced-motion and in forced-colours mode.
- No JavaScript: works in Server Components.
Changelog
- v1.0.0Latest
First release.