A soft highlight that glides around the border edge like light catching a bevel, over a faint static rim.
npx shadcn@latest add @paragon/shine-border"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export interface ShineBorderProps extends React.ComponentProps<"div"> {
/** Seconds per lap around the border. */
duration?: number;
/** Border ring thickness in px. */
borderWidth?: number;
/** Corner radius of the ring in px. Match the parent's radius. */
borderRadius?: number;
/** Overall shine opacity, 0–1. */
strength?: number;
/** Angular width of the bright arc, in degrees (10–180). */
spread?: number;
/**
* Shine color. Defaults to the theme foreground so it reads as a soft
* specular in both themes; pass "white" over brand/colored surfaces.
*/
color?: string;
/** Faint always-on base rim beneath the moving shine, 0–1. */
rim?: number;
}
const clamp = (v: number, lo: number, hi: number) =>
Math.min(Math.max(v, lo), hi);
/**
* A soft highlight that travels around the border edge, like light catching a
* bevel — quieter and rounder than border-beam's comet.
*
* Technique (border-beam engine idiom): a flat tinted ring is confined to the
* border via the double-mask (padding + content-box/exclude); a second conic
* mask — a single soft arc driven by one registered @property angle — rides
* over it, so only that stretch of the rim lights up and glides around the
* perimeter for the cost of one interpolated value. A faint static rim keeps
* the edge defined between passes. Pauses offscreen; reduced motion parks the
* arc. Absolutely positioned — parent needs position: relative.
*/
export function ShineBorder({
duration = 4,
borderWidth = 1.5,
borderRadius = 12,
strength = 1,
spread = 70,
color = "var(--color-foreground)",
rim = 0.12,
className,
style,
ref: forwardedRef,
...props
}: ShineBorderProps) {
const id = React.useId().replace(/[^a-zA-Z0-9-]/g, "");
const localRef = React.useRef<HTMLDivElement | null>(null);
const [inView, setInView] = React.useState(true);
React.useEffect(() => {
const node = localRef.current;
if (!node) return;
const observer = new IntersectionObserver(([entry]) => {
setInView(entry?.isIntersecting ?? true);
});
observer.observe(node);
return () => observer.disconnect();
}, []);
const level = clamp(strength, 0, 1);
const rimLevel = clamp(rim, 0, 1);
const arc = clamp(spread, 10, 180);
// Half-arc on each side of the head; a soft ramp in, bright crest, ramp out.
const half = arc / 2;
const q1 = (half * 0.55).toFixed(1);
const tint = (pct: number) =>
`color-mix(in oklab, ${color} ${pct}%, transparent)`;
const ringMask =
"linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)";
return (
<>
<style href={`paragon-shine-border-${id}`} precedence="paragon">{`
@property --shine-b-${id} {
syntax: "<angle>";
initial-value: 0deg;
inherits: true;
}
@keyframes shine-border-${id} {
to { --shine-b-${id}: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
[data-shineb="${id}"] { animation: none !important; }
}
`}</style>
<div
aria-hidden
data-shineb={id}
ref={(node) => {
localRef.current = node;
if (typeof forwardedRef === "function") forwardedRef(node);
else if (forwardedRef) forwardedRef.current = node;
}}
className={cn("pointer-events-none absolute inset-0", className)}
style={{
borderRadius,
opacity: level,
animation: `shine-border-${id} ${duration}s linear infinite`,
animationPlayState: inView ? "running" : "paused",
...style,
}}
{...props}
>
{/* Static base rim — keeps the edge defined between passes. */}
{rimLevel > 0 && (
<div
style={{
position: "absolute",
inset: 0,
borderRadius,
padding: borderWidth,
background: tint(100 * rimLevel * 0.6),
mask: ringMask,
WebkitMaskComposite: "xor",
maskComposite: "exclude",
}}
/>
)}
{/* Moving shine: tinted ring intersected with a soft conic arc. */}
<div
style={
{
position: "absolute",
inset: 0,
borderRadius,
padding: borderWidth,
background: tint(85),
mask: `conic-gradient(from calc(var(--shine-b-${id}) - ${half}deg), transparent 0deg, ${tint(40)} ${q1}deg, #fff ${half}deg, ${tint(40)} ${(arc - Number(q1)).toFixed(1)}deg, transparent ${arc}deg), linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
WebkitMaskComposite: "source-in, xor",
maskComposite: "intersect, exclude",
} as React.CSSProperties
}
/>
</div>
</>
);
}