Thinking...Generating your summarySearching the web
import { TextShimmer } from "@/components/pandacoderz-ui/text-shimmer";
export default function TextShimmerDemo() {
return (
<div className="flex flex-col items-start gap-4 text-sm">
<TextShimmer className="text-muted-foreground" invertLight spread={12}>
Thinking...
</TextShimmer>
<TextShimmer className="text-base font-medium text-foreground" duration={1.4} repeatDelay={0.6} invertLight>
Generating your summary
</TextShimmer>
<TextShimmer className="text-brand" color="oklch(0.85 0.1 293)" spread={25}>
Searching the web
</TextShimmer>
</div>
);
}Installation
npx shadcn@latest add https://ui.spencerwueste.com/r/text-shimmer.jsonpnpm dlx shadcn@latest add https://ui.spencerwueste.com/r/text-shimmer.jsonyarn dlx shadcn@latest add https://ui.spencerwueste.com/r/text-shimmer.jsonbunx --bun shadcn@latest add https://ui.spencerwueste.com/r/text-shimmer.jsonCopy the source into your project:
import * as React from "react";
import { cn } from "@/lib/utils";
export type TextShimmerProps = Omit<
React.HTMLAttributes<HTMLElement>,
"color"
> & {
as?: React.ElementType;
/** Duration in seconds. @default 1 */
duration?: number;
/** Spread around center in percent points. @default 20 */
spread?: number;
/** Beam angle in degrees. @default 0 */
angle?: number;
/** Override highlight color. */
color?: string;
/** Invert shimmer contrast. @default false */
invert?: boolean;
/** Invert shimmer contrast in light theme only. @default false */
invertLight?: boolean;
/** Invert shimmer contrast in dark theme only. @default false */
invertDark?: boolean;
/** Delay before the next shimmer pass in seconds. @default 0 */
repeatDelay?: number;
/** Disable shimmer animation and render plain text. @default false */
disableShimmer?: boolean;
};
export function TextShimmer({
as: Comp = "span",
className,
style,
duration = 1,
repeatDelay = 0,
spread = 20,
angle = 0,
color,
invert = false,
invertLight = false,
invertDark = false,
disableShimmer = false,
children,
...props
}: TextShimmerProps) {
const id = React.useId();
const boundedSpread = Math.min(Math.max(spread, 5), 45);
const activeDuration = Math.max(duration, 0);
const pauseDuration = Math.max(repeatDelay, 0);
const totalDuration = Math.max(activeDuration + pauseDuration, 0.001);
const movePercent = (activeDuration / totalDuration) * 100;
const keyframeName = React.useMemo(
() => `pc-text-shimmer-${id.replace(/[^a-zA-Z0-9_-]/g, "")}`,
[id],
);
const start = 50 - boundedSpread;
const end = 50 + boundedSpread;
const edge = "currentColor";
const brightBeam =
color ??
"oklch(from currentColor max(0.8, calc(l + 0.4)) c h / calc(alpha + 0.35))";
const dimBeam =
color ??
"oklch(from currentColor min(calc(l - 0.4), 0.2) c h / calc(alpha + 0.4))";
const lightBeam = invert || invertLight ? dimBeam : brightBeam;
const darkBeam = invert || invertDark ? dimBeam : brightBeam;
const keyframes = `@keyframes ${keyframeName} {
0% { background-position: 100% 50%; }
${movePercent}% { background-position: -60% 50%; }
100% { background-position: -100% 50%; }
}
[data-pc-text-shimmer="${keyframeName}"] {
--pc-text-shimmer-beam: var(--pc-text-shimmer-beam-light);
}
.dark [data-pc-text-shimmer="${keyframeName}"],
[data-theme="dark"] [data-pc-text-shimmer="${keyframeName}"] {
--pc-text-shimmer-beam: var(--pc-text-shimmer-beam-dark);
}`;
const shimmerVariables = disableShimmer
? {}
: ({
"--pc-text-shimmer-beam-light": lightBeam,
"--pc-text-shimmer-beam-dark": darkBeam,
} as React.CSSProperties);
const shimmerStyle: React.CSSProperties = disableShimmer
? {}
: {
backgroundImage: `linear-gradient(${90 + angle}deg, ${edge} ${start}%, var(--pc-text-shimmer-beam) 50%, ${edge} ${end}%)`,
animation: `${keyframeName} ${totalDuration}s linear infinite`,
WebkitTextFillColor: "transparent",
};
return (
<>
{!disableShimmer ? <style>{keyframes}</style> : null}
<Comp
data-pc-text-shimmer={keyframeName}
className={cn(
!disableShimmer && "bg-size-[200%_auto] bg-clip-text",
className,
)}
style={
{
...shimmerVariables,
...shimmerStyle,
...style,
} as React.CSSProperties
}
{...props}
>
{children}
</Comp>
</>
);
}Usage
import { TextShimmer } from "@/components/pandacoderz-ui/text-shimmer";<TextShimmer className="text-muted-foreground" invertLight>
Thinking...
</TextShimmer>The beam color derives from currentColor, so it adapts to whatever text color you apply. Use invertLight or invertDark when the default beam has too little contrast on one theme.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
as |
ElementType |
span |
Element to render. |
duration |
number |
1 |
Seconds per pass. |
repeatDelay |
number |
0 |
Pause between passes, in seconds. |
spread |
number |
20 |
Beam width in percent points (5–45). |
angle |
number |
0 |
Beam angle in degrees. |
color |
string |
Explicit beam color. | |
invert / invertLight / invertDark |
boolean |
false |
Use a dark beam instead of a bright one. |
disableShimmer |
boolean |
false |
Render plain text. |