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
- Paste the snippet just before
</head>. - Add
data-neon-buttonto a button or link on a dark background.
Options
Add any of these attributes to the same element.
data-color | Neon colour. Default #22d3ee. |
data-flicker="false" | No flickering: it just glows. |
data-autoplay | It 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.
<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><button data-neon-button>Enter</button>
<!-- in pink, without the flicker: -->
<button data-neon-button data-color="#ff3fd8" data-flicker="false">Play</button>