A flagship CTA with a slanted highlight band that sweeps on an ease-then-rest cycle — token-driven for both themes, paused offscreen, gone under reduced motion.
npx shadcn@latest add @paragon/shimmer-button"use client";
import * as React from "react";
import { useInView } from "motion/react";
import { cn } from "@/lib/utils";
/*
* One static keyframe drives every instance; per-instance color/duration
* arrive through custom properties, so the <style> dedupes to a single tag.
* The band eases across (0 -> 60%) then rests — a shimmer that never pauses
* reads as noise, one that breathes reads as intent.
*/
const shimmerStyles = `
[data-slot="shimmer-button"] {
box-shadow: inset 0 1px 0 0 color-mix(in oklch, var(--color-primary-foreground) 16%, transparent);
}
[data-slot="shimmer-button"] > [data-shimmer-band] {
background: linear-gradient(
115deg,
transparent 20%,
color-mix(in oklch, var(--pg-shimmer-color) 38%, transparent) 50%,
transparent 80%
);
animation: pg-shimmer-sweep var(--pg-shimmer-duration, 2.8s) infinite;
}
@keyframes pg-shimmer-sweep {
0% { translate: -120% 0; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
60%, 100% { translate: 280% 0; }
}
@media (prefers-reduced-motion: reduce) {
[data-slot="shimmer-button"] > [data-shimmer-band] {
animation: none !important;
opacity: 0;
}
}
`;
export interface ShimmerButtonProps extends React.ComponentProps<"button"> {
/** Highlight color of the traveling band. Defaults to the label token. */
shimmerColor?: string;
/** Seconds per sweep, including the rest between passes. */
duration?: number;
/** Freezes the shimmer; press physics stay. */
static?: boolean;
}
/**
* The flagship CTA. A slanted highlight band sweeps the surface on an
* ease-then-rest cycle, riding tokens so it holds up in both themes. The
* loop pauses offscreen, dies under reduced motion, and the band is a
* decorative transform-only layer — the button underneath is the house
* button: press scale, focus ring, enumerated transitions.
*/
export function ShimmerButton({
shimmerColor,
duration = 2.8,
static: isStatic = false,
className,
style,
children,
...props
}: ShimmerButtonProps) {
const ref = React.useRef<HTMLButtonElement>(null);
const inView = useInView(ref, { amount: 0.1 });
return (
<>
<style href="paragon-shimmer-button" precedence="paragon">
{shimmerStyles}
</style>
<button
ref={ref}
type="button"
data-slot="shimmer-button"
className={cn(
"relative inline-flex h-10 items-center justify-center gap-2 overflow-hidden rounded-lg bg-primary px-6 text-sm font-medium whitespace-nowrap text-primary-foreground",
"transition-[background-color,box-shadow,scale] duration-150 ease-out",
"hover:bg-primary/90",
"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"disabled:pointer-events-none disabled:opacity-50",
"active:not-disabled:scale-[0.97]",
"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className,
)}
style={
{
"--pg-shimmer-color":
shimmerColor ?? "var(--color-primary-foreground)",
"--pg-shimmer-duration": `${duration}s`,
...style,
} as React.CSSProperties
}
{...props}
>
<span
aria-hidden
data-shimmer-band=""
className="pointer-events-none absolute inset-y-0 left-0 w-1/2"
style={{
animationPlayState: !inView || isStatic ? "paused" : undefined,
opacity: isStatic ? 0 : undefined,
}}
/>
<span className="relative inline-flex items-center gap-2">
{children}
</span>
</button>
</>
);
}