effects
Text Shimmer
A sweep of luminance — a single soft highlight band glides left-to-right across a readable dim base (--foreground) on a slow infinite loop via background-clip:text. The band is tinted toward --primary (a soft sheen, not a harsh loading flash) and the oversized background-size sweeps the peak across while the base text stays readable throughout the cycle. For hero headings, labels, and status text. Configurable tag, duration, spread, and colors; disabled prop renders static text.
Generating response
Install
1. Register the namespace (once per project):
json
// components.json — register the @bottega namespace once
{
"registries": {
"@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
}
}2. Add the component:
bash
npx shadcn add @bottega/text-shimmerUsage
Usagetsx
<TextShimmer>
{/* children */}
</TextShimmer>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children* | React.ReactNode | — | Text content to shimmer. Required. |
| as | React.ElementType | span | Rendered element tag. Default: "span". Accepts "p", "h1"–"h6", "div". |
| duration | number | 2.4 | Full sweep cycle in seconds. Default: 2.4. Longer = more refined. |
| spread | number | 0.35 | Width of the shimmer band as a fraction of total text width (0–1). Default: 0.35. |
| shimmerColor | string | var(--primary) | Peak color of the sweep band. Default: var(--primary). Any CSS color. |
| baseColor | string | — | Rest color of the text. Default: 45% foreground (dim but readable). Any CSS color. |
| disabled | boolean | false | If true, renders plain static text with baseColor, no animation. |
| className | string | Merged into the root element for sizing/layout overrides. |
Source
text-shimmer.tsxtsx
"use client";
import { createElement, useEffect, useState } from "react";
import { useReducedMotion } from "motion/react";
import type React from "react";
import styles from "./text-shimmer.module.css";
export type TextShimmerProps = {
/** Text content to shimmer. Required. */
children: React.ReactNode;
/** Rendered element tag. Default: "span". Accepts "p", "h1"–"h6", "div". */
as?: React.ElementType;
/** Full sweep cycle in seconds. Default: 2.4. Longer = more refined. */
duration?: number;
/** Width of the shimmer band as a fraction of total text width (0–1). Default: 0.35. */
spread?: number;
/** Peak color of the sweep band. Default: var(--primary). Any CSS color. */
shimmerColor?: string;
/** Rest color of the text. Default: 45% foreground (dim but readable). Any CSS color. */
baseColor?: string;
/** If true, renders plain static text with baseColor, no animation. */
disabled?: boolean;
/** Merged into the root element for sizing/layout overrides. */
className?: string;
} & Omit<React.HTMLAttributes<HTMLElement>, "color">;
export function TextShimmer({
children,
as: Tag = "span",
duration = 2.4,
spread = 0.35,
shimmerColor = "var(--primary)",
baseColor,
disabled = false,
className = "",
style,
...props
}: TextShimmerProps) {
// Mount-gate + null-safe reduced-motion check (contract §47). The animated
// class is only applied AFTER hydration for non-reduced-motion users, so the
// server HTML and the client's first render are byte-identical static text
// (no React #418), and reduced-motion users never see a flash.
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const prefersReducedMotion = useReducedMotion();
const reduce = prefersReducedMotion === null || prefersReducedMotion; // null => reduced
const animate = mounted && !reduce && !disabled;
// Clamp spread to the valid fraction range, then map to a half-band percent
// offset from the gradient centre (50%). spread 0.35 -> ±17.5%.
const clampedSpread = Math.min(Math.max(spread, 0), 1);
const spreadPercent = `${(clampedSpread * 50).toFixed(2)}%`;
// Polymorphic tag via createElement — avoids JSX widening `as` over every
// intrinsic element (which collapses `children` to `never`).
return createElement(
Tag,
{
className: [styles.shimmer, animate ? styles.animated : "", className]
.filter(Boolean)
.join(" "),
// Inline custom properties consumed by the CSS module. Colors default
// to contract tokens; consumers may override with any CSS color.
style: {
"--shimmer-base": baseColor,
"--shimmer-peak": shimmerColor,
"--shimmer-duration": `${duration}s`,
"--shimmer-spread": spreadPercent,
...style,
} as React.CSSProperties,
...props,
},
children,
);
}
Dependencies
- motion
- @bottega/tokens