Gild
A metallic light sweep crosses glyphs on a deliberate slow interval — a white-hot core edged in molten amber and ember red over a brushed-bronze base. Pure CSS, driven by an animated @property.
cssfree
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { useReducedMotion } from "@/registry/default/hooks/use-reduced-motion/use-reduced-motion";
export interface GildColors {
/** Resting metal tone the sweep travels across. */
base?: string;
/** White-hot core of the sweep. */
core?: string;
/** Molten-amber inner edge of the sweep. */
amber?: string;
/** Ember-red outer edge of the sweep. */
ember?: string;
}
export interface GildProps extends React.ComponentPropsWithoutRef<"span"> {
/** Element tag rendered. @default "span" */
as?: "span" | "div" | "p" | "h1" | "h2" | "h3";
/** Gradient stops for the base tone and sweep core/edges. */
colors?: GildColors;
/** Seconds between sweeps. @default 4.5 */
interval?: number;
/** Sweep angle in degrees. @default 100 */
angle?: number;
/** Only sweep while hovered, instead of continuously on an interval. @default false */
hoverOnly?: boolean;
/** Freeze the sweep at its current frame. */
paused?: boolean;
}
const DEFAULT_COLORS: Required<GildColors> = {
base: "#8a6a4b",
core: "#fff8ef",
amber: "#ff6b35",
ember: "#c1121f",
};
/**
* Gild — a narrow white-hot light band, edged in molten amber and ember red,
* sweeps across a brushed-bronze text fill on a deliberate slow interval —
* never a constant conveyor shimmer. Pure CSS, driven by an animated
* `@property` so the sweep interpolates smoothly inside the gradient stops.
* Freezes at a static mid-sweep frame under reduced motion.
*/
export function Gild({
as = "span",
colors,
interval = 4.5,
angle = 100,
hoverOnly = false,
paused = false,
className,
style,
children,
...props
}: GildProps) {
const reducedMotion = useReducedMotion();
const c = { ...DEFAULT_COLORS, ...colors };
const Tag = as;
const gradient = `linear-gradient(${angle}deg,
${c.base} 0%,
${c.base} calc(var(--gild-shine, 45%) - 22%),
${c.amber} calc(var(--gild-shine, 45%) - 8%),
${c.core} var(--gild-shine, 45%),
${c.amber} calc(var(--gild-shine, 45%) + 8%),
${c.ember} calc(var(--gild-shine, 45%) + 16%),
${c.base} calc(var(--gild-shine, 45%) + 30%),
${c.base} 100%)`;
const forcePaused = paused || reducedMotion;
return (
<Tag
data-crucible="gild"
data-hover-only={hoverOnly ? "" : undefined}
className={cn("gild-text inline-block bg-clip-text text-transparent", className)}
style={
{
backgroundImage: gradient,
backgroundSize: "220% 220%",
...(forcePaused ? { "--gild-play": "paused" } : {}),
...(reducedMotion ? { "--gild-shine": "45%" } : {}),
...style,
} as React.CSSProperties
}
{...props}
>
<style>{`
/* Rest at -35% so every sweep stop (widest: shine + 30%) sits below 0%
and the resting fill is pure base — no stuck ember glyph. */
@property --gild-shine {
syntax: '<percentage>';
inherits: true;
initial-value: -35%;
}
@keyframes crucible-gild-sweep {
0% { --gild-shine: -35%; }
30% { --gild-shine: 120%; }
100% { --gild-shine: 120%; }
}
[data-crucible="gild"] {
animation: crucible-gild-sweep ${interval}s ease-in-out infinite;
animation-play-state: var(--gild-play, running);
}
[data-crucible="gild"][data-hover-only] {
animation-play-state: var(--gild-play, paused);
}
[data-crucible="gild"][data-hover-only]:hover {
animation-play-state: var(--gild-play, running);
}
@media (prefers-reduced-motion: reduce) {
[data-crucible="gild"] {
animation: none;
--gild-shine: 45%;
}
}
`}</style>
{children}
</Tag>
);
}
Installation
CLI
npx shadcn@latest add @crucible/gildHonors prefers-reduced-motion with a designed static fallback, and passes className through.