Neon Text Effect
Original · freeneon glow text
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/txfx-neon.json"use client";
import { useId, useMemo, useRef, useState, type KeyboardEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
type NeonTube = {
id: string;
label: string;
hex: string;
swatch: string;
};
const TUBES: NeonTube[] = [
{ id: "rose", label: "Hot Rose", hex: "#ff2e88", swatch: "bg-rose-500" },
{ id: "cyan", label: "Ice Cyan", hex: "#22d3ee", swatch: "bg-sky-400" },
{ id: "green", label: "Toxic Green", hex: "#22ff9c", swatch: "bg-emerald-400" },
{ id: "violet", label: "Ultra Violet", hex: "#a855f7", swatch: "bg-violet-500" },
{ id: "amber", label: "Amber Buzz", hex: "#ffb020", swatch: "bg-amber-400" },
];
function tubeGlow(hex: string, strength: number, lit: boolean): string {
if (!lit) return "0 0 1px rgba(0,0,0,0.4)";
const s = 0.5 + strength * 1.15;
const p = (n: number) => `${(n * s).toFixed(1)}px`;
return [
`0 0 ${p(1.5)} #fff`,
`0 0 ${p(5)} #fff`,
`0 0 ${p(11)} ${hex}`,
`0 0 ${p(22)} ${hex}`,
`0 0 ${p(42)} ${hex}`,
`0 0 ${p(78)} ${hex}`,
].join(", ");
}
export default function TxfxNeon() {
const reduce = useReducedMotion();
const uid = useId();
const [tubeId, setTubeId] = useState<string>("rose");
const [message, setMessage] = useState<string>("MIDNIGHT");
const [strength, setStrength] = useState<number>(72);
const [lit, setLit] = useState<boolean>(true);
const [flicker, setFlicker] = useState<boolean>(true);
const tubeRefs = useRef<(HTMLButtonElement | null)[]>([]);
const tube = useMemo(
() => TUBES.find((t) => t.id === tubeId) ?? TUBES[0],
[tubeId],
);
const s = strength / 100;
const sign = message.trim() === "" ? "OPEN" : message;
const litColor = lit ? "#fbf5ff" : "#2b2b31";
const mainShadow = tubeGlow(tube.hex, s, lit);
const subShadow = tubeGlow(tube.hex, s * 0.55, lit);
const animClass =
lit && !reduce ? (flicker ? "txfx-neon-flicker" : "txfx-neon-hum") : "";
const onTubeKey = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {
const nav = ["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp", "Home", "End"];
if (!nav.includes(e.key)) return;
e.preventDefault();
let next = index;
if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (index + 1) % TUBES.length;
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = (index - 1 + TUBES.length) % TUBES.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = TUBES.length - 1;
setTubeId(TUBES[next].id);
tubeRefs.current[next]?.focus();
};
const ring =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2";
return (
<section className="relative w-full overflow-hidden bg-slate-100 px-6 py-20 text-slate-900 sm:py-28 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes txfxNeonFlicker {
0%, 16%, 18%, 22%, 24%, 52%, 54%, 100% { opacity: 1; }
17%, 23%, 53% { opacity: 0.32; }
21% { opacity: 0.72; }
78%, 80% { opacity: 0.5; }
79% { opacity: 1; }
}
@keyframes txfxNeonHum {
0%, 100% { opacity: 1; }
50% { opacity: 0.9; }
}
@keyframes txfxNeonHalo {
0%, 100% { opacity: 0.55; transform: translate(-50%, -50%) scale(1); }
50% { opacity: 0.85; transform: translate(-50%, -50%) scale(1.06); }
}
.txfx-neon-flicker { animation: txfxNeonFlicker 4.2s infinite steps(1, end); }
.txfx-neon-hum { animation: txfxNeonHum 3s ease-in-out infinite; }
.txfx-neon-halo { animation: txfxNeonHalo 4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.txfx-neon-flicker, .txfx-neon-hum, .txfx-neon-halo { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-5xl">
<header className="mb-10 text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-300 bg-white/70 px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.2em] text-slate-600 backdrop-blur dark:border-slate-700 dark:bg-slate-900/70 dark:text-slate-300">
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5 fill-rose-500" aria-hidden="true">
<path d="M13 2 3 14h6l-2 8 10-12h-6l2-8Z" />
</svg>
Neon Sign Studio
</span>
<h2 className="mt-5 text-3xl font-bold tracking-tight sm:text-4xl">
Wire up your own glowing sign
</h2>
<p className="mx-auto mt-3 max-w-lg text-base leading-relaxed text-slate-600 dark:text-slate-400">
Type a message, swap the tube color, dial the glow, and flip the
power. Everything below is live and keyboard-friendly.
</p>
</header>
{/* Neon stage */}
<motion.div
initial={reduce ? false : { opacity: 0, y: 18 }}
whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="relative overflow-hidden rounded-3xl border border-slate-800 bg-slate-950 p-8 shadow-2xl sm:p-14"
style={{
backgroundImage:
"radial-gradient(120% 90% at 50% 0%, rgba(30,41,59,0.9) 0%, rgba(2,6,23,1) 62%), repeating-linear-gradient(90deg, rgba(148,163,184,0.05) 0px, rgba(148,163,184,0.05) 1px, transparent 1px, transparent 26px)",
}}
>
{/* colored ambient halo */}
<div
aria-hidden="true"
className={`pointer-events-none absolute left-1/2 top-1/2 h-72 w-72 -translate-x-1/2 -translate-y-1/2 rounded-full blur-3xl ${
lit && !reduce ? "txfx-neon-halo" : ""
}`}
style={{
backgroundColor: tube.hex,
opacity: lit ? 0.28 + s * 0.3 : 0,
transition: "opacity 0.4s ease, background-color 0.4s ease",
}}
/>
<div className="relative flex flex-col items-center text-center">
<span
aria-hidden="true"
className="mb-6 text-[0.7rem] font-semibold uppercase tracking-[0.5em] text-slate-500"
style={{ color: lit ? tube.hex : undefined, textShadow: subShadow }}
>
Est. Downtown
</span>
<div
className={animClass}
style={{
color: litColor,
textShadow: mainShadow,
transition: "color 0.35s ease",
willChange: "opacity",
}}
>
<span
aria-label={`Neon sign reading ${sign}`}
role="img"
className="block break-words text-5xl font-black uppercase leading-none tracking-tight sm:text-7xl"
style={{ fontFamily: "'Trebuchet MS', 'Segoe UI', system-ui, sans-serif" }}
>
{sign}
</span>
<span
aria-hidden="true"
className="mt-5 block text-sm font-semibold uppercase tracking-[0.35em] sm:text-base"
style={{ textShadow: subShadow }}
>
Cocktails · Music · Till 3 AM
</span>
</div>
{/* reflection */}
<div
aria-hidden="true"
className="mt-4 select-none text-5xl font-black uppercase leading-none tracking-tight text-slate-100/80 blur-[2px] sm:text-7xl"
style={{
color: litColor,
textShadow: mainShadow,
transform: "scaleY(-1)",
opacity: lit ? 0.16 : 0.05,
maskImage: "linear-gradient(to bottom, rgba(0,0,0,0.5), transparent 70%)",
WebkitMaskImage: "linear-gradient(to bottom, rgba(0,0,0,0.5), transparent 70%)",
fontFamily: "'Trebuchet MS', 'Segoe UI', system-ui, sans-serif",
}}
>
{sign}
</div>
{/* power indicator */}
<div className="mt-8 inline-flex items-center gap-2 rounded-full border border-slate-700 bg-slate-900/60 px-3 py-1.5 text-xs font-medium text-slate-400">
<span
className="h-2 w-2 rounded-full"
style={{
backgroundColor: lit ? "#22ff9c" : "#64748b",
boxShadow: lit ? "0 0 8px #22ff9c" : "none",
}}
/>
{lit ? "Transformer live" : "Power cut"}
</div>
</div>
</motion.div>
{/* Controls */}
<div className="mt-8 grid gap-6 rounded-3xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8 dark:border-slate-800 dark:bg-slate-900/60 lg:grid-cols-2">
{/* Message */}
<div className="lg:col-span-2">
<label
htmlFor={`${uid}-msg`}
className="mb-2 block text-sm font-semibold text-slate-700 dark:text-slate-200"
>
Sign text
</label>
<input
id={`${uid}-msg`}
type="text"
value={message}
maxLength={14}
onChange={(e) => setMessage(e.target.value.toUpperCase())}
placeholder="OPEN"
autoComplete="off"
spellCheck={false}
className={`w-full rounded-xl border border-slate-300 bg-slate-50 px-4 py-3 text-lg font-bold uppercase tracking-widest text-slate-900 placeholder:text-slate-400 focus-visible:ring-rose-500 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100 dark:placeholder:text-slate-600 dark:focus-visible:ring-offset-slate-900 ${ring}`}
/>
<p className="mt-1.5 text-xs text-slate-500 dark:text-slate-400">
{message.length}/14 characters
</p>
</div>
{/* Tube color */}
<div>
<span
id={`${uid}-tube-label`}
className="mb-2 block text-sm font-semibold text-slate-700 dark:text-slate-200"
>
Tube color
</span>
<div
role="radiogroup"
aria-labelledby={`${uid}-tube-label`}
className="flex flex-wrap gap-2"
>
{TUBES.map((t, i) => {
const selected = t.id === tubeId;
return (
<button
key={t.id}
ref={(el) => {
tubeRefs.current[i] = el;
}}
type="button"
role="radio"
aria-checked={selected}
aria-label={t.label}
tabIndex={selected ? 0 : -1}
onClick={() => setTubeId(t.id)}
onKeyDown={(e) => onTubeKey(e, i)}
className={`group inline-flex items-center gap-2 rounded-full border px-3 py-2 text-sm font-medium transition focus-visible:ring-slate-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${ring} ${
selected
? "border-slate-900 bg-slate-900 text-white dark:border-slate-100 dark:bg-slate-100 dark:text-slate-900"
: "border-slate-300 bg-white text-slate-700 hover:border-slate-400 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-slate-600"
}`}
>
<span
className={`h-4 w-4 rounded-full ${t.swatch}`}
style={{ boxShadow: `0 0 8px ${t.hex}` }}
aria-hidden="true"
/>
{t.label}
</button>
);
})}
</div>
</div>
{/* Glow strength */}
<div>
<label
htmlFor={`${uid}-strength`}
className="mb-2 flex items-center justify-between text-sm font-semibold text-slate-700 dark:text-slate-200"
>
<span>Glow strength</span>
<span className="tabular-nums text-slate-500 dark:text-slate-400">
{strength}%
</span>
</label>
<input
id={`${uid}-strength`}
type="range"
min={0}
max={100}
step={1}
value={strength}
onChange={(e) => setStrength(Number(e.target.value))}
className={`w-full cursor-pointer rounded-full focus-visible:ring-rose-500 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${ring}`}
style={{ accentColor: tube.hex }}
/>
{/* Toggles */}
<div className="mt-6 flex flex-col gap-4 sm:flex-row sm:gap-8">
<div className="flex items-center gap-3">
<button
type="button"
role="switch"
aria-checked={lit}
aria-label="Power"
onClick={() => setLit((v) => !v)}
className={`relative h-7 w-12 shrink-0 rounded-full border transition-colors focus-visible:ring-emerald-500 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${ring} ${
lit
? "border-emerald-500 bg-emerald-500"
: "border-slate-300 bg-slate-300 dark:border-slate-600 dark:bg-slate-700"
}`}
>
<span
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-all ${
lit ? "left-[calc(100%-1.375rem)]" : "left-0.5"
}`}
/>
</button>
<span className="text-sm font-medium text-slate-700 dark:text-slate-200">
Power
</span>
</div>
<div className="flex items-center gap-3">
<button
type="button"
role="switch"
aria-checked={flicker}
aria-label="Flicker"
disabled={!lit}
onClick={() => setFlicker((v) => !v)}
className={`relative h-7 w-12 shrink-0 rounded-full border transition-colors focus-visible:ring-amber-500 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${ring} ${
flicker && lit
? "border-amber-500 bg-amber-500"
: "border-slate-300 bg-slate-300 dark:border-slate-600 dark:bg-slate-700"
} ${!lit ? "cursor-not-allowed opacity-50" : ""}`}
>
<span
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-all ${
flicker ? "left-[calc(100%-1.375rem)]" : "left-0.5"
}`}
/>
</button>
<span className="text-sm font-medium text-slate-700 dark:text-slate-200">
Flicker
</span>
</div>
</div>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Aurora Gradient Headline
OriginalAn animated headline whose living multi-colour gradient drifts across every letter while the words blur and stagger into place on scroll.

Character Reveal Heading
OriginalA cinematic heading that blurs and lifts in one character at a time with a staggered, scroll-triggered reveal.

Shimmer Sweep Headline
OriginalA headline with a polished light sheen that glides endlessly across the letters and races faster on hover.

Rotating Words Headline
OriginalA headline whose final word flips through a curated set with a blurred vertical roll while the rest of the line stays perfectly still.

Gradient Animate Text Effect
Originalanimated gradient text

Typing Text Effect
Originaltypewriter text with caret

Shiny Text Effect
Originalshiny sweep text

Glitch Text Effect
Originalglitch text effect

Wave Text Effect
Originalwavy letter animation

Reveal Mask Text Effect
Originalmask reveal heading

Outline Text Effect
Originaloutline-to-fill text

3D Text Effect
Originallayered 3D text

