← Buttons

Neon Button

An outlined button that glows like a neon tube, with a blurred reflection of its light on the surface below. It flickers to life as it comes into view and stutters now and then like a real tube. Hover it and the tube floods with light.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-neon-button to a button or link on a dark background.

Options

Add any of these attributes to the same element.

data-colorNeon colour. Default #22d3ee.
data-flicker="false"No flickering: it just glows.
data-autoplayIt lights up by itself every few seconds while in view. Handy for demos.

Leave a little room underneath for the reflection. Keyboard focus lights it too, and on touch screens a tap lights it for a moment.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Neon Button: an outlined button that glows like a neon tube, with a
    // blurred reflection of its light on the surface below. It flickers to
    // life as it comes into view, and now and then stutters like a real tube.
    // Hover it and the tube floods with light, the text turns dark and the
    // reflection brightens. Keyboard focus and taps light it too.
    // Usage: <button data-neon-button>Enter</button>
    // Optional: data-color (neon colour, default #22d3ee), data-flicker="false"
    // (no flickering), data-autoplay (it lights up by itself every few seconds
    // while in view; handy for demos).
    // Put it on a dark background. With reduced motion it simply glows.
    var DEFAULTS = { color: '#22d3ee', every: 3200 };

    var css =
      '.neb{position:relative;background:transparent;color:var(--neb-c);border:2px solid color-mix(in srgb,var(--neb-c) 30%,transparent);' +
      'text-shadow:0 0 .4em var(--neb-c),0 0 1em var(--neb-c);transition:background-color .3s,color .3s,box-shadow .3s}' +
      '.neb-glow{position:absolute;inset:-2px;border-radius:inherit;border:2px solid var(--neb-c);pointer-events:none;' +
      'box-shadow:0 0 .5em var(--neb-c),inset 0 0 .5em var(--neb-c);transition:box-shadow .3s}' +
      '.neb-refl{position:absolute;left:4%;right:4%;top:calc(100% + .7em);height:100%;border-radius:inherit;background:var(--neb-c);' +
      'pointer-events:none;filter:blur(1em);transform:perspective(1.2em) rotateX(40deg) scale(1,.35);opacity:.5;transition:opacity .3s}' +
      '.neb.neb-on{background:var(--neb-c);color:#0a0a12;text-shadow:none;box-shadow:0 0 1.2em var(--neb-c),0 0 3em color-mix(in srgb,var(--neb-c) 60%,transparent)}' +
      '.neb.neb-on .neb-glow{box-shadow:0 0 1em var(--neb-c),inset 0 0 .4em var(--neb-c)}' +
      '.neb.neb-on .neb-refl{opacity:.85}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function setup(el) {
      var d = el.dataset;
      var c = d.color || DEFAULTS.color;
      el.classList.add('neb');
      el.style.setProperty('--neb-c', c);
      var glow = document.createElement('span');
      glow.className = 'neb-glow';
      var refl = document.createElement('span');
      refl.className = 'neb-refl';
      [glow, refl].forEach(function (s) { s.setAttribute('aria-hidden', 'true'); el.appendChild(s); });

      // How brightly the tube is lit, 0 to 1.
      function light(i) {
        glow.style.opacity = i;
        refl.style.opacity = i * (el.classList.contains('neb-on') ? 0.85 : 0.5);
        el.style.textShadow = i > 0.3 ? '0 0 .4em ' + c + ', 0 0 1em ' + c : 'none';
        el.style.opacity = 0.55 + i * 0.45;
      }
      function clear() { glow.style.opacity = refl.style.opacity = el.style.textShadow = el.style.opacity = ''; }

      // A flicker is a run of [brightness, ms] steps, like a tube striking.
      var flickering = false;
      function flicker(steps) {
        if (flickering) return;
        flickering = true;
        var n = 0;
        (function step() {
          if (n >= steps.length) { flickering = false; clear(); return; }
          light(steps[n][0]);
          setTimeout(step, steps[n++][1]);
        })();
      }
      var STRIKE = [[0, 260], [0.8, 60], [0.05, 90], [0.9, 50], [0.1, 160], [1, 70], [0.3, 50], [1, 0]];
      var STUTTER = [[0.25, 50], [1, 70], [0.15, 60], [1, 0]];

      var lit = false;
      function on(v) { if (v === lit) return; lit = v; el.classList.toggle('neb-on', v); }
      function busy() { return el.matches(':hover') || document.activeElement === el; }

      el.addEventListener('pointerenter', function (e) { if (e.pointerType !== 'touch') on(true); });
      el.addEventListener('pointerleave', function (e) { if (e.pointerType !== 'touch' && document.activeElement !== el) on(false); });
      el.addEventListener('focus', function () { on(true); });
      el.addEventListener('blur', function () { if (!el.matches(':hover')) on(false); });
      var touchTimer = null;
      el.addEventListener('pointerdown', function (e) {
        if (e.pointerType !== 'touch') return;
        clearTimeout(touchTimer);
        on(true);
        touchTimer = setTimeout(function () { on(false); }, 1400);
      });

      if (reduced()) return;
      var flickers = d.flicker !== 'false';
      var autoplay = d.autoplay !== undefined && d.autoplay !== 'false';
      if (flickers) light(0);
      var struck = false, timer = null, beat = 0;
      var tick = function () {
        if (!busy()) {
          beat++;
          if (autoplay && beat % 2) {
            on(true);
            setTimeout(function () { if (!busy()) on(false); }, DEFAULTS.every * 0.45);
          } else if (flickers && Math.random() < 0.7) {
            flicker(STUTTER);
          }
        }
        timer = setTimeout(tick, DEFAULTS.every);
      };
      new IntersectionObserver(function (entries) {
        clearTimeout(timer);
        if (!entries[0].isIntersecting) return;
        if (!struck && flickers) { struck = true; flicker(STRIKE); }
        if (autoplay || flickers) timer = setTimeout(tick, autoplay ? 1400 : DEFAULTS.every);
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-neon-button]').forEach(function (el) {
        if (el._neb) return;
        el._neb = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<button data-neon-button>Enter</button>

<!-- in pink, without the flicker: -->
<button data-neon-button data-color="#ff3fd8" data-flicker="false">Play</button>