Argon
SVG gas-discharge tube lettering — ignition travels along each glyph's tube with a seeded strike flicker, then holds with a faint mains hum; the letter under the pointer destabilises. Neon, argon and mercury presets; fully lit and steady under reduced motion.
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";
/** Built-in gas presets, or any CSS color string. */
export type ArgonGas = "neon" | "argon" | "mercury" | (string & {});
export interface ArgonHandle {
/** Switch every tube off and run the ignition intro again. */
replay(): void;
/** Destabilise one glyph (by index among visible glyphs), or a seeded random one. */
flicker(index?: number): void;
}
export interface ArgonProps extends Omit<React.HTMLAttributes<HTMLElement>, "children"> {
/** The lettering. Use "\n" for a stacked multi-line sign. @default "Open late" */
text?: string;
/** Gas fill: "neon" (red-orange), "argon" (ice blue), "mercury" (cold white), or any CSS color. @default "argon" */
gas?: ArgonGas;
/** Element the (visually hidden) real text is rendered as, so it stays a heading. @default "h2" */
as?: "h1" | "h2" | "h3" | "p";
/** Glyph size in px at natural size; the sign scales down to fit its container. @default 96 */
fontSize?: number;
/** Lit tube stroke width in px at natural size. @default 2.4 */
strokeWidth?: number;
/** Render mounting clips beneath each glyph and a back-wire behind every line. @default true */
clips?: boolean;
/** Wait until the sign scrolls into view before igniting (otherwise ignite on mount). @default true */
igniteOnView?: boolean;
/** Glow strength: bloom size/opacity and interior gas fill. 0–2. @default 1 */
intensity?: number;
/** When no pointer has been present for ~1.5s, glyphs occasionally destabilise on their own. @default true */
idle?: boolean;
/** Hold the sign fully lit and steady — no intro, hum, or flicker. @default false */
paused?: boolean;
/** Imperative handle: `replay()` and `flicker(index?)`. */
ref?: React.Ref<ArgonHandle>;
}
const GAS_PRESETS: Record<string, string> = {
neon: "#ff5a3c",
argon: "#7cc4ff",
mercury: "#e8f1ff",
};
/** Stagger between glyph ignitions (ms). */
const STAGGER = 70;
/** Time for the ignition to travel the dash window (ms). */
const DRAW_MS = 1400;
/** Dash window, in em — longer than any glyph's outline. */
const DASH_EM = 10;
/** Photosensitivity budget: max opacity dips from hover/idle flicker in any 1s window. */
const MAX_DIPS_PER_SECOND = 3;
const FLICKER_DIPS = 2;
interface Glyph {
char: string;
line: number;
/** Start x relative to the line's left edge. */
x: number;
/** Advance width. */
w: number;
}
interface Layout {
glyphs: Glyph[];
lineWidths: number[];
measured: boolean;
}
function mulberry32(seed: number) {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function hashString(s: string): number {
let h = 2166136261;
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return h >>> 0;
}
/** Deterministic pre-measure layout (SSR + first client render; hidden until measured). */
function estimateLayout(lines: string[], fontSize: number): Layout {
const glyphs: Glyph[] = [];
const lineWidths: number[] = [];
const adv = fontSize * 0.6;
lines.forEach((line, li) => {
const chars = Array.from(line);
chars.forEach((char, ci) => {
if (!/\s/.test(char)) glyphs.push({ char, line: li, x: ci * adv, w: adv });
});
lineWidths.push(chars.length * adv);
});
return { glyphs, lineWidths, measured: false };
}
/** Seeded strike: 2–3 uneven partial dips inside the first ~200ms of a glyph lighting. */
function strikeKeyframes(rng: () => number): Keyframe[] {
const dips = rng() < 0.5 ? 2 : 3;
const total = 240;
const frames: Keyframe[] = [{ opacity: 0, offset: 0 }];
let t = 18 + rng() * 14;
frames.push({ opacity: 0.85 + rng() * 0.15, offset: t / total });
for (let d = 0; d < dips; d++) {
t += 22 + rng() * 22;
frames.push({ opacity: 0.2 + rng() * 0.25, offset: Math.min(t, 200) / total });
t += 18 + rng() * 22;
frames.push({ opacity: 0.8 + rng() * 0.2, offset: Math.min(t, 210) / total });
}
frames.push({ opacity: 1, offset: 1 });
return frames;
}
/** Seeded ~400ms destabilise: exactly two partial dips, then a wobble back to steady. */
function flickerKeyframes(rng: () => number): Keyframe[] {
return [
{ opacity: 1, offset: 0 },
{ opacity: 0.28 + rng() * 0.14, offset: 0.08 + rng() * 0.04 },
{ opacity: 0.88 + rng() * 0.1, offset: 0.2 },
{ opacity: 0.72 + rng() * 0.1, offset: 0.34 },
{ opacity: 0.38 + rng() * 0.14, offset: 0.46 + rng() * 0.04 },
{ opacity: 1, offset: 0.62 },
{ opacity: 0.9 + rng() * 0.06, offset: 0.8 },
{ opacity: 1, offset: 1 },
];
}
/**
* Argon — a headline rendered as bent gas-discharge glass tubes. Each glyph
* is an SVG text outline in three layers: a dim unlit tube, a bright lit core
* with a white-hot center, and a blurred bloom. On view, ignition travels
* along each tube glyph-by-glyph with an uneven strike flicker, then holds
* with a near-imperceptible mains hum. The glyph nearest the pointer
* destabilises briefly. Flicker is photosensitivity-budgeted (≤3 dips/s);
* reduced motion renders the sign fully lit and steady.
*/
export function Argon({
text = "Open late",
gas = "argon",
as = "h2",
fontSize = 96,
strokeWidth = 2.4,
clips = true,
igniteOnView = true,
intensity = 1,
idle = true,
paused = false,
ref,
className,
style,
...props
}: ArgonProps) {
const reducedMotion = useReducedMotion();
const staticMode = reducedMotion || paused;
const Tag = as;
const uid = React.useId().replace(/:/g, "");
const filterId = `argon-bloom-${uid}`;
const maskId = `argon-mask-${uid}`;
const lines = React.useMemo(() => text.split("\n"), [text]);
const seed = React.useMemo(() => hashString(text), [text]);
const color = GAS_PRESETS[gas] ?? gas;
const hot = `color-mix(in srgb, ${color} 42%, #ffffff)`;
const k = Math.max(0, Math.min(2, intensity));
const [layout, setLayout] = React.useState<Layout>(() => estimateLayout(lines, fontSize));
const [igniteKey, setIgniteKey] = React.useState(0);
const [lit, setLit] = React.useState(false);
const [inView, setInView] = React.useState(false);
const [docVisible, setDocVisible] = React.useState(true);
const svgRef = React.useRef<SVGSVGElement>(null);
const measureRefs = React.useRef<(SVGTextElement | null)[]>([]);
const dashRefs = React.useRef<(SVGTextElement | null)[][]>([]);
const glowRefs = React.useRef<(SVGGElement | null)[]>([]);
const bloomRefs = React.useRef<(SVGTextElement | null)[]>([]);
const fillRefs = React.useRef<(SVGTextElement | null)[]>([]);
const playedKeyRef = React.useRef(0);
const activeFlickers = React.useRef(new Map<number, Animation>());
const dipTimes = React.useRef<number[]>([]);
const flickerCount = React.useRef(0);
const lastPointer = React.useRef(-Infinity);
const hoverIndex = React.useRef(-1);
// ---- geometry -----------------------------------------------------------
const pad = Math.max(strokeWidth * 6, fontSize * 0.2);
const lineHeight = fontSize * 1.18;
const ascent = fontSize * 0.8;
const clipSpace = clips ? fontSize * 0.2 : 0;
const maxLineWidth = Math.max(1, ...layout.lineWidths);
const vbW = maxLineWidth + pad * 2;
const vbH = pad * 2 + ascent + (lines.length - 1) * lineHeight + fontSize * 0.22 + clipSpace;
const baselineOf = (line: number) => pad + ascent + line * lineHeight;
const lineOffset = (line: number) => pad + (maxLineWidth - (layout.lineWidths[line] ?? 0)) / 2;
const dashLen = fontSize * DASH_EM;
// ---- measure real glyph advances (kerning + letter-spacing included) -----
const measure = React.useCallback(() => {
const glyphs: Glyph[] = [];
const lineWidths: number[] = [];
lines.forEach((line, li) => {
const el = measureRefs.current[li];
if (!el) {
lineWidths.push(0);
return;
}
let width = 0;
try {
width = el.getComputedTextLength();
} catch {
width = 0;
}
lineWidths.push(width);
let unit = 0;
for (const char of Array.from(line)) {
if (!/\s/.test(char)) {
try {
const start = el.getStartPositionOfChar(unit).x;
const end = el.getEndPositionOfChar(unit + char.length - 1).x;
glyphs.push({ char, line: li, x: start, w: Math.max(0, end - start) });
} catch {
/* unaddressable character — skip */
}
}
unit += char.length;
}
});
setLayout({ glyphs, lineWidths, measured: true });
}, [lines]);
React.useLayoutEffect(() => {
measure();
const fonts = typeof document !== "undefined" ? document.fonts : undefined;
if (!fonts) return;
let alive = true;
const onLoad = () => alive && measure();
fonts.ready.then(onLoad).catch(() => {});
fonts.addEventListener?.("loadingdone", onLoad);
return () => {
alive = false;
fonts.removeEventListener?.("loadingdone", onLoad);
};
}, [measure, fontSize]);
// ---- visibility (in view + tab visible) ---------------------------------
React.useEffect(() => {
const node = svgRef.current;
if (!node) return;
if (typeof IntersectionObserver === "undefined") {
setInView(true);
return;
}
const io = new IntersectionObserver(([entry]) => setInView(!!entry?.isIntersecting), {
threshold: 0.35,
});
io.observe(node);
return () => io.disconnect();
}, []);
React.useEffect(() => {
const onVis = () => setDocVisible(document.visibilityState !== "hidden");
onVis();
document.addEventListener("visibilitychange", onVis);
return () => document.removeEventListener("visibilitychange", onVis);
}, []);
// ---- ignition trigger ---------------------------------------------------
React.useEffect(() => {
if (igniteKey !== 0) return;
if (!igniteOnView || inView) setIgniteKey(1);
}, [igniteOnView, inView, igniteKey]);
// Static mode (reduced motion / paused): everything lit, nothing animating.
React.useEffect(() => {
if (!staticMode) return;
playedKeyRef.current = Math.max(playedKeyRef.current, igniteKey);
activeFlickers.current.forEach((a) => a.cancel());
activeFlickers.current.clear();
setLit(true);
}, [staticMode, igniteKey]);
const glyphCount = layout.glyphs.length;
const measured = layout.measured;
React.useLayoutEffect(() => {
if (staticMode || !measured || igniteKey === 0 || playedKeyRef.current === igniteKey) return;
playedKeyRef.current = igniteKey;
setLit(false);
activeFlickers.current.forEach((a) => a.cancel());
activeFlickers.current.clear();
const anims: Animation[] = [];
const len = fontSize * DASH_EM;
for (let i = 0; i < glyphCount; i++) {
const delay = i * STAGGER;
const rng = mulberry32(seed ^ Math.imul(i + 1, 0x9e3779b1) ^ Math.imul(igniteKey, 0x85ebca6b));
for (const el of dashRefs.current[i] ?? []) {
if (!el?.animate) continue;
anims.push(
el.animate([{ strokeDashoffset: len }, { strokeDashoffset: 0 }], {
duration: DRAW_MS,
delay,
easing: "cubic-bezier(0.3, 0.15, 0.55, 1)",
fill: "both",
})
);
}
const strike = strikeKeyframes(rng);
const g = glowRefs.current[i];
const b = bloomRefs.current[i];
if (g?.animate) anims.push(g.animate(strike, { duration: 240, delay, fill: "both" }));
if (b?.animate) anims.push(b.animate(strike, { duration: 240, delay, fill: "both" }));
const f = fillRefs.current[i];
if (f?.animate) {
anims.push(
f.animate([{ opacity: 0 }, { opacity: 1 }], {
duration: 520,
delay: delay + DRAW_MS * 0.3,
easing: "ease-out",
fill: "both",
})
);
}
}
let cancelled = false;
Promise.all(anims.map((a) => a.finished))
.then(() => {
if (cancelled) return;
setLit(true);
// Committed state now matches the end frames — release the animations.
anims.forEach((a) => a.cancel());
})
.catch(() => {});
return () => {
cancelled = true;
anims.forEach((a) => a.cancel());
};
}, [igniteKey, staticMode, measured, glyphCount, seed, fontSize]);
// ---- flicker (hover / idle / handle) ------------------------------------
const flicker = React.useCallback(
(index?: number) => {
if (staticMode || !lit || glyphCount === 0) return;
const n = flickerCount.current++;
const rng = mulberry32(seed ^ Math.imul(n + 17, 0x27d4eb2f));
const i =
index === undefined ? Math.floor(rng() * glyphCount) : Math.max(0, Math.min(glyphCount - 1, index));
if (activeFlickers.current.has(i)) return;
const now = performance.now();
dipTimes.current = dipTimes.current.filter((t) => now - t < 1000);
if (dipTimes.current.length + FLICKER_DIPS > MAX_DIPS_PER_SECOND) return;
const frames = flickerKeyframes(rng);
const opts: KeyframeAnimationOptions = { duration: 400, easing: "linear" };
const g = glowRefs.current[i];
const b = bloomRefs.current[i];
const f = fillRefs.current[i];
if (!g?.animate) return;
dipTimes.current.push(now, now + 180);
const a = g.animate(frames, opts);
b?.animate?.(frames, opts);
f?.animate?.(frames, opts);
activeFlickers.current.set(i, a);
const done = () => {
if (activeFlickers.current.get(i) === a) activeFlickers.current.delete(i);
};
a.finished.then(done, done);
},
[staticMode, lit, glyphCount, seed]
);
React.useImperativeHandle(
ref,
() => ({
replay: () => {
if (staticMode) return;
setIgniteKey((key) => key + 1);
},
flicker,
}),
[flicker, staticMode]
);
// Idle ghost: the sign occasionally destabilises on its own when nobody's pointing.
const running = lit && !staticMode && inView && docVisible;
React.useEffect(() => {
if (!running || !idle) return;
const rng = mulberry32(seed ^ 0x51ed27);
let timer: ReturnType<typeof setTimeout>;
const schedule = () => {
timer = setTimeout(
() => {
if (performance.now() - lastPointer.current > 1500) flicker();
schedule();
},
2600 + rng() * 3200
);
};
schedule();
return () => clearTimeout(timer);
}, [running, idle, seed, flicker]);
React.useEffect(() => {
const flickers = activeFlickers.current;
return () => {
flickers.forEach((a) => a.cancel());
flickers.clear();
};
}, []);
const nearestGlyph = (clientX: number, clientY: number) => {
const svg = svgRef.current;
const ctm = svg?.getScreenCTM();
if (!svg || !ctm) return -1;
const p = new DOMPoint(clientX, clientY).matrixTransform(ctm.inverse());
let best = -1;
let bestD = fontSize * 0.9;
layout.glyphs.forEach((g, i) => {
const cx = lineOffset(g.line) + g.x + g.w / 2;
const cy = baselineOf(g.line) - fontSize * 0.36;
const d = Math.hypot((p.x - cx) * 1.15, p.y - cy);
if (d < bestD) {
bestD = d;
best = i;
}
});
return best;
};
const onPointerMove = (e: React.PointerEvent<SVGSVGElement>) => {
lastPointer.current = performance.now();
const i = nearestGlyph(e.clientX, e.clientY);
if (i !== hoverIndex.current) {
hoverIndex.current = i;
if (i >= 0) flicker(i);
}
};
const onPointerDown = (e: React.PointerEvent<SVGSVGElement>) => {
lastPointer.current = performance.now();
const i = nearestGlyph(e.clientX, e.clientY);
if (i >= 0) flicker(i);
};
const onPointerLeave = () => {
lastPointer.current = performance.now();
hoverIndex.current = -1;
};
// ---- render -------------------------------------------------------------
const restOffset = staticMode || lit ? 0 : dashLen;
const restFill = staticMode || lit ? 1 : 0;
const humming = lit && !staticMode;
const bloomBlur = strokeWidth * (0.9 + 1.4 * k);
const tubeTransition = "stroke 450ms ease, fill 450ms ease";
const textProps = (g: Glyph) => ({
x: lineOffset(g.line) + g.x,
y: baselineOf(g.line),
});
return (
<Tag
data-crucible="argon"
className={cn("relative inline-block max-w-full font-semibold", className)}
style={style}
{...props}
>
<span className="sr-only">{text}</span>
<style>{`
@keyframes crucible-argon-hum {
0%, 100% { opacity: 1; }
23% { opacity: 0.997; }
41% { opacity: 1; }
67% { opacity: 0.9975; }
84% { opacity: 0.999; }
}
@keyframes crucible-argon-hum-bloom {
0%, 100% { opacity: 1; }
31% { opacity: 0.99; }
58% { opacity: 1; }
79% { opacity: 0.992; }
}
[data-crucible="argon"] [data-hum] {
animation: crucible-argon-hum 2.3s ease-in-out infinite;
animation-play-state: var(--argon-play, running);
}
[data-crucible="argon"] [data-hum="bloom"] {
animation-name: crucible-argon-hum-bloom;
animation-duration: 3.1s;
}
@media (prefers-reduced-motion: reduce) {
[data-crucible="argon"] [data-hum] { animation: none; }
}
`}</style>
<svg
ref={svgRef}
aria-hidden
focusable="false"
width={vbW}
viewBox={`0 0 ${vbW} ${vbH}`}
className="block h-auto max-w-full select-none overflow-visible"
style={
{
visibility: layout.measured ? "visible" : "hidden",
"--argon-play": running ? "running" : "paused",
} as React.CSSProperties
}
onPointerMove={onPointerMove}
onPointerDown={onPointerDown}
onPointerLeave={onPointerLeave}
>
<defs>
<filter id={filterId} x="-30%" y="-40%" width="160%" height="180%" colorInterpolationFilters="sRGB">
<feGaussianBlur in="SourceGraphic" stdDeviation={bloomBlur} result="near" />
<feGaussianBlur in="SourceGraphic" stdDeviation={bloomBlur * 3.4} result="far" />
<feMerge>
<feMergeNode in="far" />
<feMergeNode in="near" />
</feMerge>
</filter>
{/* Hides the inner half of every outline stroke — tubes read as clean
rims, and overlapping contours in variable fonts never show. */}
<mask id={maskId} maskUnits="userSpaceOnUse" x={0} y={0} width={vbW} height={vbH}>
<rect x={0} y={0} width={vbW} height={vbH} fill="#fff" />
<g style={{ fontSize }} fontFamily="inherit" fontWeight="inherit" fill="#000" stroke="none">
{layout.glyphs.map((g, i) => (
<text key={i} {...textProps(g)}>
{g.char}
</text>
))}
</g>
</mask>
</defs>
<g style={{ fontSize }} fontFamily="inherit" fontWeight="inherit" strokeLinejoin="round" strokeLinecap="round">
{/* measuring copies — never painted */}
<g visibility="hidden">
{lines.map((line, li) => (
<text
key={li}
ref={(el) => {
measureRefs.current[li] = el;
}}
x={0}
y={baselineOf(li)}
style={{ whiteSpace: "pre" }}
>
{line}
</text>
))}
</g>
{/* back-wire + mounting clips */}
{clips && (
<g>
{lines.map((_, li) => {
const lineGlyphs = layout.glyphs.filter((g) => g.line === li);
if (lineGlyphs.length === 0) return null;
const first = lineGlyphs[0]!;
const last = lineGlyphs[lineGlyphs.length - 1]!;
const off = lineOffset(li);
const wy = baselineOf(li) + fontSize * 0.15;
return (
<line
key={li}
x1={off + first.x - fontSize * 0.08}
x2={off + last.x + last.w + fontSize * 0.08}
y1={wy}
y2={wy}
stroke="rgba(255,255,255,0.09)"
strokeWidth={Math.max(1, fontSize * 0.012)}
/>
);
})}
{layout.glyphs.map((g, i) => {
const cx = lineOffset(g.line) + g.x + g.w / 2;
const by = baselineOf(g.line);
const cw = fontSize * 0.085;
const ch = fontSize * 0.06;
return (
<g key={i}>
<line
x1={cx}
x2={cx}
y1={by + ch * 0.6}
y2={by + fontSize * 0.15}
stroke="rgba(255,255,255,0.12)"
strokeWidth={Math.max(1, fontSize * 0.01)}
/>
<rect
x={cx - cw / 2}
y={by - ch * 0.35}
width={cw}
height={ch}
rx={ch * 0.3}
style={{
fill: `color-mix(in srgb, ${color} ${restFill ? 14 : 4}%, #2b2b30)`,
stroke: "rgba(255,255,255,0.2)",
transition: "fill 600ms ease",
}}
strokeWidth={Math.max(0.75, fontSize * 0.006)}
/>
</g>
);
})}
</g>
)}
{/* unlit glass tubes */}
{layout.glyphs.map((g, i) => (
<text key={i} {...textProps(g)} fill="rgba(255,255,255,0.03)" stroke="none">
{g.char}
</text>
))}
<g mask={`url(#${maskId})`}>
{layout.glyphs.map((g, i) => (
<text
key={i}
{...textProps(g)}
fill="none"
stroke="rgba(226,232,240,0.2)"
strokeWidth={strokeWidth * 1.5}
>
{g.char}
</text>
))}
</g>
{/* bloom */}
<g filter={`url(#${filterId})`} opacity={Math.min(1, 0.6 * k)} data-hum={humming ? "bloom" : undefined}>
{layout.glyphs.map((g, i) => (
<text
key={i}
ref={(el) => {
bloomRefs.current[i] = el;
(dashRefs.current[i] ??= [])[0] = el;
}}
{...textProps(g)}
fill="none"
strokeWidth={strokeWidth * (2.2 + k)}
strokeDasharray={`${dashLen} ${dashLen}`}
style={{ stroke: color, strokeDashoffset: restOffset, transition: tubeTransition }}
>
{g.char}
</text>
))}
</g>
{/* gas glowing inside the glass */}
<g data-hum={humming ? "core" : undefined}>
{layout.glyphs.map((g, i) => (
<text
key={i}
ref={(el) => {
fillRefs.current[i] = el;
}}
{...textProps(g)}
stroke="none"
style={{
fill: color,
fillOpacity: 0.08 * k,
opacity: restFill,
transition: tubeTransition,
}}
>
{g.char}
</text>
))}
</g>
{/* lit tubes: core rim + white-hot inner edge (outer half only, via mask) */}
<g data-hum={humming ? "core" : undefined} mask={`url(#${maskId})`}>
{layout.glyphs.map((g, i) => (
<g
key={i}
ref={(el) => {
glowRefs.current[i] = el;
}}
>
<text
ref={(el) => {
(dashRefs.current[i] ??= [])[1] = el;
}}
{...textProps(g)}
fill="none"
strokeWidth={strokeWidth * 2}
strokeDasharray={`${dashLen} ${dashLen}`}
style={{
stroke: color,
strokeOpacity: Math.min(1, 0.65 + 0.35 * k),
strokeDashoffset: restOffset,
transition: tubeTransition,
}}
>
{g.char}
</text>
<text
ref={(el) => {
(dashRefs.current[i] ??= [])[2] = el;
}}
{...textProps(g)}
fill="none"
strokeWidth={strokeWidth * 0.9}
strokeDasharray={`${dashLen} ${dashLen}`}
style={{ stroke: hot, strokeDashoffset: restOffset, transition: tubeTransition }}
>
{g.char}
</text>
</g>
))}
</g>
</g>
</svg>
</Tag>
);
}
Installation
CLI
npx shadcn@latest add @crucible/argonProps
| Prop | Type | Default | Description |
|---|---|---|---|
| text | string | "Open late" | The lettering. Use "\n" for a stacked multi-line sign. |
| gas | ArgonGas | "argon" | Gas fill: "neon" (red-orange), "argon" (ice blue), "mercury" (cold white), or any CSS color. |
| as | "h1" | "h2" | "h3" | "p" | "h2" | Element the (visually hidden) real text is rendered as, so it stays a heading. |
| fontSize | number | 96 | Glyph size in px at natural size; the sign scales down to fit its container. |
| strokeWidth | number | 2.4 | Lit tube stroke width in px at natural size. |
| clips | boolean | true | Render mounting clips beneath each glyph and a back-wire behind every line. |
| igniteOnView | boolean | true | Wait until the sign scrolls into view before igniting (otherwise ignite on mount). |
| intensity | number | 1 | Glow strength: bloom size/opacity and interior gas fill. 0–2. |
| idle | boolean | true | When no pointer has been present for ~1.5s, glyphs occasionally destabilise on their own. |
| paused | boolean | false | Hold the sign fully lit and steady — no intro, hum, or flicker. |
| ref | React.Ref<ArgonHandle> | Imperative handle: replay() and flicker(index?). | |
Also accepts all props of Omit<React.HTMLAttributes<HTMLElement>, "children"> — they pass through to the underlying element. | |||
Honors prefers-reduced-motion with a designed static fallback, and passes className through.