Sheen
A shimmer-sweep button. A light periodically crosses the face and brightens the rim like a heat pass, cooling through an ember afterglow back to matte. Pure CSS, zero dependencies.
cssfree
import * as React from "react";
import { cn } from "@/lib/utils";
export interface SheenProps extends React.ComponentPropsWithoutRef<"button"> {
/** Peak color of the heat pass, at its brightest. Any CSS color. */
color?: string;
/** Color the rim decays through as the pass's heat lingers and fades. */
afterglowColor?: string;
/** Seconds between the start of each pass. */
interval?: number;
/** Border radius in px. */
radius?: number;
/** Freeze the animation, holding the current frame. */
paused?: boolean;
}
/**
* Sheen — a shimmer-sweep button. Pure CSS, zero dependencies. A light
* periodically crosses the face and brightens the rim like a blacksmith's
* heat pass, then the border cools through an ember afterglow back to fully
* matte — never a constant conveyor shimmer. Honors prefers-reduced-motion
* by freezing on a faint static rim glow.
*/
export function Sheen({
color = "#ff6b35",
afterglowColor = "#c1121f",
interval = 3.5,
radius = 10,
paused = false,
className,
style,
disabled,
children,
...rest
}: SheenProps) {
return (
<button
data-crucible="sheen"
className={cn(
"relative isolate inline-flex items-center justify-center gap-2 overflow-hidden",
"bg-neutral-900 px-6 py-3 text-sm font-medium text-white",
"transition-colors duration-200 select-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-950",
"focus-visible:ring-[color:var(--sheen-color)]",
"disabled:cursor-not-allowed disabled:opacity-50",
className
)}
style={
{
"--sheen-color": color,
"--sheen-afterglow": afterglowColor,
"--sheen-interval": `${interval}s`,
"--sheen-play": paused ? "paused" : "running",
borderRadius: radius,
...style,
} as React.CSSProperties
}
disabled={disabled}
{...rest}
>
<style>{`
@keyframes crucible-sheen-rim {
0%, 6% { border-color: rgba(255, 255, 255, 0.08); box-shadow: 0 0 0 0 transparent; }
9% { border-color: var(--sheen-color); box-shadow: 0 0 14px 1px color-mix(in oklab, var(--sheen-color) 65%, transparent); }
22% { border-color: var(--sheen-afterglow); box-shadow: 0 0 9px 0px color-mix(in oklab, var(--sheen-afterglow) 45%, transparent); }
32%, 100% { border-color: rgba(255, 255, 255, 0.08); box-shadow: 0 0 0 0 transparent; }
}
@keyframes crucible-sheen-band {
0%, 6% { transform: translateX(-140%) skewX(-18deg); }
9% { transform: translateX(140%) skewX(-18deg); }
100% { transform: translateX(140%) skewX(-18deg); }
}
[data-crucible="sheen"] {
border: 1px solid rgba(255, 255, 255, 0.08);
transition: border-color 200ms ease, background-color 200ms ease, box-shadow 200ms ease,
transform 120ms ease, filter 120ms ease;
}
[data-crucible="sheen"]:hover:not(:disabled) {
border-color: color-mix(in oklab, var(--sheen-color) 45%, rgba(255, 255, 255, 0.08));
background-color: #222226;
box-shadow: 0 0 12px 0 color-mix(in oklab, var(--sheen-color) 22%, transparent);
}
[data-crucible="sheen"]:active:not(:disabled) {
transform: scale(0.98);
filter: brightness(0.9);
}
[data-crucible="sheen"] .sheen-rim {
animation: crucible-sheen-rim var(--sheen-interval) ease-out infinite;
animation-play-state: var(--sheen-play);
}
[data-crucible="sheen"] .sheen-band {
animation: crucible-sheen-band var(--sheen-interval) ease-in infinite;
animation-play-state: var(--sheen-play);
}
[data-crucible="sheen"]:disabled .sheen-rim,
[data-crucible="sheen"]:disabled .sheen-band {
animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
[data-crucible="sheen"] .sheen-rim,
[data-crucible="sheen"] .sheen-band {
animation: none;
}
[data-crucible="sheen"] .sheen-rim {
box-shadow: 0 0 8px 0px color-mix(in oklab, var(--sheen-color) 30%, transparent);
border-color: color-mix(in oklab, var(--sheen-color) 35%, transparent);
}
[data-crucible="sheen"] .sheen-band {
display: none;
}
}
`}</style>
{/* Rim: idle matte border that flashes and cools on every pass. */}
<span
aria-hidden
className="sheen-rim pointer-events-none absolute inset-0 rounded-[inherit] border border-transparent"
/>
{/* Sweep: a diagonal band that crosses the face and brightens the label. */}
<span
aria-hidden
className="pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]"
>
<span
className="sheen-band absolute top-1/2 left-0 h-[220%] w-1/3 -translate-y-1/2"
style={{
background:
"linear-gradient(90deg, transparent, color-mix(in oklab, var(--sheen-color) 85%, white) 50%, transparent)",
mixBlendMode: "screen",
opacity: 0.8,
}}
/>
</span>
<span className="relative z-10">{children}</span>
</button>
);
}
Installation
CLI
npx shadcn@latest add @crucible/sheenHonors prefers-reduced-motion with a designed static fallback, and passes className through.