Skip to content
Uxelith
Sign in
Freev1.0.0Text & Motion

Shimmer Text

Text with a band of light that sweeps across it, sized to the text so short labels and long headings move at the same pace. Settles to plain text when the work is done. Pure CSS, renders on the server.

pnpm dlx shadcn add …/r/shimmer-text.json
Licence
MIT
Packages
2
Updated
10 Oct 2026
Live

Installation

  1. Run this in a project set up with shadcn/ui:

    $ pnpm dlx shadcn@latest add https://uxelith.com/r/shimmer-text.json
  2. It adds these packages for you:

    clsxtailwind-merge
  3. The source lands in your components folder as shimmer-text.tsx. It's yours: change anything.

Usage

example.tsx
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.

shimmer-text.tsx
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

PropTypeDefaultDescription
as"h1" | "h2" | "h3" | "h4" | "p" | "span""span"Element to render, so headings stay headings.
durationnumber2.5Seconds for one sweep.
spreadnumberfrom text lengthWidth of the band of light, in pixels.
activebooleantrueSet false when the work is done; the text settles to its normal colour.
childrenReactNode—The text.
--shimmer-baseCSS variable--muted-foregroundText colour outside the light.
--shimmer-lightCSS variable--foregroundColour 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

  1. v1.0.0Latest

    First release.

More in Text & Motion

All 4 in Text & Motion
  • Text & MotionFree

    Blur Text

    Text that comes into focus word by word or letter by letter, then brings out the words you choose: they take the accent colour while a line draws beneath them. Screen readers get the sentence once.

  • Text & MotionFree

    Marquee

    Content that loops past across or up and down with no gaps, at a steady speed, with soft faded edges. Pauses on hover and keyboard focus.

  • Text & MotionFree

    Typewriter

    Text that types itself out with a human rhythm, a caret that holds steady while typing and blinks when idle, and words that are backspaced or selected and replaced. Starts when scrolled into view; screen readers get the sentence once.