Web InnoventixFreeCode

Neon Text Effect

Original · free

neon glow text

byWeb InnoventixReact + Tailwind
txfxneontext-effects
Open

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
txfx-neon.tsx
"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

motion

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 quote

Similar components

Browse all →