Tesla
import { useEffect, useRef, useState } from 'react';
const css = `
@keyframes btn-tesla-flash {
from { box-shadow: 0 0 0 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 70%, transparent), 0 0 40px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 50%, transparent); background-color: light-dark(#dbdde1, #2e3035); }
to { box-shadow: 0 0 0 14px transparent, 0 0 0 transparent; background-color: light-dark(#f3f4f7, #1a1c1f); }
}
.btn-tesla { position: relative; width: 260px; height: 120px; }
.btn-tesla__arcs { position: absolute; inset: 0; overflow: visible; pointer-events: none; }
.btn-tesla__arc {
fill: none;
stroke: light-dark(#373a40, #f1f2f3);
stroke-width: 1.6;
stroke-linejoin: bevel;
filter: drop-shadow(0 0 3px light-dark(#373a40, #f1f2f3)) drop-shadow(0 0 8px light-dark(#50535b, #c6c8cd));
}
.btn-tesla__arc--faint { stroke-width: 0.8; opacity: 0.55; }
.btn-tesla__node {
fill: light-dark(#ebedf0, #232529);
stroke: light-dark(#5f626a, #a7abb2);
stroke-width: 1.5;
}
.btn-tesla__node[data-live] { fill: light-dark(#373a40, #f1f2f3); filter: drop-shadow(0 0 6px light-dark(#373a40, #f1f2f3)); }
.btn-tesla__btn {
position: absolute;
left: 50px;
top: 50px;
width: 160px;
height: 46px;
border: 1px solid light-dark(#cbced3, #393c41);
border-radius: 10px;
background: light-dark(#f3f4f7, #1a1c1f);
color: light-dark(#373a40, #f1f2f3);
font-family: inherit;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.06em;
cursor: pointer;
}
.btn-tesla__btn:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.btn-tesla__btn[data-flash] { animation: btn-tesla-flash 0.45s ease-out; }
`;
const L = { x: 38, y: 73 };
const R = { x: 222, y: 73 };
/** A jagged bolt from a to b, bowed over the top of the button by `lift` px. */
function bolt(a: { x: number; y: number }, b: { x: number; y: number }, lift: number, jitter: number) {
const n = 16;
const cx = (a.x + b.x) / 2;
const cy = Math.min(a.y, b.y) - lift;
let d = `M${a.x} ${a.y}`;
for (let i = 1; i < n; i++) {
const t = i / n;
// Point on the quadratic curve a → (cx, cy) → b, pushed off it at random.
const x = (1 - t) ** 2 * a.x + 2 * (1 - t) * t * cx + t ** 2 * b.x;
const y = (1 - t) ** 2 * a.y + 2 * (1 - t) * t * cy + t ** 2 * b.y;
const k = Math.sin(t * Math.PI) * jitter;
d += ` L${(x + (Math.random() - 0.5) * k).toFixed(1)} ${(y + (Math.random() - 0.5) * k * 1.4).toFixed(1)}`;
}
return `${d} L${b.x} ${b.y}`;
}
export function TeslaButton() {
const [arcs, setArcs] = useState<string[]>([]);
const [flash, setFlash] = useState(false);
const live = useRef(false);
const burst = useRef(0);
useEffect(() => {
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
let idle = 0;
const id = window.setInterval(() => {
const now = performance.now();
if (now < burst.current) {
setArcs([bolt(L, R, 104, 30), bolt(L, R, 90, 34), bolt(L, R, 122, 24), bolt(L, R, 78, 22)]);
} else if (live.current) {
setArcs([bolt(L, R, 100, 20), bolt(L, R, 92, 28)]);
} else {
// At rest: an occasional short crackle off the left electrode.
idle += 1;
const spark = idle % 36 < 2;
setArcs(spark ? [bolt(L, { x: L.x + 22 + Math.random() * 14, y: L.y - 26 }, 6, 10)] : []);
}
}, 55);
return () => clearInterval(id);
}, []);
return (
<>
<style>
{css}
</style>
<div
className="btn-tesla"
onPointerEnter={() => (live.current = true)}
onPointerLeave={() => (live.current = false)}
>
<svg className="btn-tesla__arcs" viewBox="0 0 260 120" aria-hidden="true">
{arcs.map((d, i) => (
<path key={i} d={d} className={`btn-tesla__arc${i % 2 ? ' btn-tesla__arc--faint' : ''}`} />
))}
<circle cx={L.x} cy={L.y} r={5} className="btn-tesla__node" data-live={arcs.length > 0 || undefined} />
<circle cx={R.x} cy={R.y} r={5} className="btn-tesla__node" data-live={arcs.length > 1 || undefined} />
</svg>
<button
type="button"
className="btn-tesla__btn"
data-flash={flash || undefined}
onAnimationEnd={() => setFlash(false)}
onFocus={() => (live.current = true)}
onBlur={() => (live.current = false)}
onClick={() => {
burst.current = performance.now() + 380;
// Off then on next frame, so back-to-back clicks restart the flash.
setFlash(false);
requestAnimationFrame(() => setFlash(true));
}}
>
Discharge
</button>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initBtnTesla(el) on the mounted element instead.
<div class="btn-tesla">
<svg class="btn-tesla__arcs" viewBox="0 0 260 120" aria-hidden="true">
<circle cx="38" cy="73" r="5" class="btn-tesla__node"></circle>
<circle cx="222" cy="73" r="5" class="btn-tesla__node"></circle>
</svg>
<button type="button" class="btn-tesla__btn">Discharge</button>
</div>@keyframes btn-tesla-flash {
from { box-shadow: 0 0 0 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 70%, transparent), 0 0 40px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 50%, transparent); background-color: light-dark(#dbdde1, #2e3035); }
to { box-shadow: 0 0 0 14px transparent, 0 0 0 transparent; background-color: light-dark(#f3f4f7, #1a1c1f); }
}
.btn-tesla { position: relative; width: 260px; height: 120px; }
.btn-tesla__arcs { position: absolute; inset: 0; overflow: visible; pointer-events: none; }
.btn-tesla__arc {
fill: none;
stroke: light-dark(#373a40, #f1f2f3);
stroke-width: 1.6;
stroke-linejoin: bevel;
filter: drop-shadow(0 0 3px light-dark(#373a40, #f1f2f3)) drop-shadow(0 0 8px light-dark(#50535b, #c6c8cd));
}
.btn-tesla__arc--faint { stroke-width: 0.8; opacity: 0.55; }
.btn-tesla__node {
fill: light-dark(#ebedf0, #232529);
stroke: light-dark(#5f626a, #a7abb2);
stroke-width: 1.5;
}
.btn-tesla__node[data-live] { fill: light-dark(#373a40, #f1f2f3); filter: drop-shadow(0 0 6px light-dark(#373a40, #f1f2f3)); }
.btn-tesla__btn {
position: absolute;
left: 50px;
top: 50px;
width: 160px;
height: 46px;
border: 1px solid light-dark(#cbced3, #393c41);
border-radius: 10px;
background: light-dark(#f3f4f7, #1a1c1f);
color: light-dark(#373a40, #f1f2f3);
font-family: inherit;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.06em;
cursor: pointer;
}
.btn-tesla__btn:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.btn-tesla__btn[data-flash] { animation: btn-tesla-flash 0.45s ease-out; }function initBtnTesla(root) {
var svg = root.querySelector('.btn-tesla__arcs');
var btn = root.querySelector('.btn-tesla__btn');
var nodes = root.querySelectorAll('.btn-tesla__node');
if (!svg || !btn || nodes.length < 2) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var NS = 'http://www.w3.org/2000/svg';
var L = { x: 38, y: 73 };
var R = { x: 222, y: 73 };
var live = false;
var burst = 0;
var idle = 0;
var group = document.createElementNS(NS, 'g');
svg.insertBefore(group, svg.firstChild);
// A jagged bolt from a to b, bowed over the top of the button by `lift` px.
function bolt(a, b, lift, jitter) {
var n = 16;
var cx = (a.x + b.x) / 2;
var cy = Math.min(a.y, b.y) - lift;
var d = 'M' + a.x + ' ' + a.y;
for (var i = 1; i < n; i++) {
var t = i / n;
var x = (1 - t) * (1 - t) * a.x + 2 * (1 - t) * t * cx + t * t * b.x;
var y = (1 - t) * (1 - t) * a.y + 2 * (1 - t) * t * cy + t * t * b.y;
var k = Math.sin(t * Math.PI) * jitter;
d += ' L' + (x + (Math.random() - 0.5) * k).toFixed(1) + ' ' + (y + (Math.random() - 0.5) * k * 1.4).toFixed(1);
}
return d + ' L' + b.x + ' ' + b.y;
}
function draw(arcs) {
group.textContent = '';
arcs.forEach(function (d, i) {
var p = document.createElementNS(NS, 'path');
p.setAttribute('d', d);
p.setAttribute('class', 'btn-tesla__arc' + (i % 2 ? ' btn-tesla__arc--faint' : ''));
group.appendChild(p);
});
nodes[0].toggleAttribute('data-live', arcs.length > 0);
nodes[1].toggleAttribute('data-live', arcs.length > 1);
}
var timer = setInterval(function () {
if (performance.now() < burst) {
draw([bolt(L, R, 104, 30), bolt(L, R, 90, 34), bolt(L, R, 122, 24), bolt(L, R, 78, 22)]);
} else if (live) {
draw([bolt(L, R, 100, 20), bolt(L, R, 92, 28)]);
} else {
// At rest: an occasional short crackle off the left electrode.
idle += 1;
draw(idle % 36 < 2 ? [bolt(L, { x: L.x + 22 + Math.random() * 14, y: L.y - 26 }, 6, 10)] : []);
}
}, 55);
function on() { live = true; }
function off() { live = false; }
function discharge() {
burst = performance.now() + 380;
// Off, then on next frame, so back-to-back clicks restart the flash.
btn.removeAttribute('data-flash');
requestAnimationFrame(function () { btn.setAttribute('data-flash', ''); });
}
function ended() { btn.removeAttribute('data-flash'); }
root.addEventListener('pointerenter', on);
root.addEventListener('pointerleave', off);
btn.addEventListener('focus', on);
btn.addEventListener('blur', off);
btn.addEventListener('click', discharge);
btn.addEventListener('animationend', ended);
return function () {
clearInterval(timer);
group.remove();
root.removeEventListener('pointerenter', on);
root.removeEventListener('pointerleave', off);
btn.removeEventListener('focus', on);
btn.removeEventListener('blur', off);
btn.removeEventListener('click', discharge);
btn.removeEventListener('animationend', ended);
};
}
document.querySelectorAll('.btn-tesla').forEach(initBtnTesla);Goal: Create a "Tesla" button.
Visual behavior: A button between two electrodes. At rest a short spark crackles now and then; on hover live arcs leap across the top of the button, redrawn many times a second, and a click discharges a burst of bolts with a flash.
Technique:
- An SVG behind the button holds the electrodes and the bolts. A bolt is a 16-point polyline along a quadratic curve that bows over the button, each point pushed off the curve at random (most in the middle).
- An interval regenerates the bolts every 55ms: two while hovered, four for 380ms after a click, and an occasional short crackle off the left electrode at rest.
- Alternate bolts are thinner and fainter, and every bolt gets a stacked drop-shadow glow; an electrode lights while an arc touches it.
- The click flash is a one-shot keyframe on the button (a spreading ring and a bright background), restarted by toggling the attribute off then on the next frame.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. Focus makes it live like hover. Under prefers-reduced-motion no arcs are drawn.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Tesla" button.
Visual behavior: A button between two electrodes. At rest a short spark crackles now and then; on hover live arcs leap across the top of the button, redrawn many times a second, and a click discharges a burst of bolts with a flash.
Technique:
- An SVG behind the button holds the electrodes and the bolts. A bolt is a 16-point polyline along a quadratic curve that bows over the button, each point pushed off the curve at random (most in the middle).
- An interval regenerates the bolts every 55ms: two while hovered, four for 380ms after a click, and an occasional short crackle off the left electrode at rest.
- Alternate bolts are thinner and fainter, and every bolt gets a stacked drop-shadow glow; an electrode lights while an arc touches it.
- The click flash is a one-shot keyframe on the button (a spreading ring and a bright background), restarted by toggling the attribute off then on the next frame.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. Focus makes it live like hover. Under prefers-reduced-motion no arcs are drawn.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A button between two electrodes. At rest a short spark crackles now and then; on hover live arcs leap across the top of the button, redrawn many times a second, and a click discharges a burst of bolts with a flash.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- hover, click, glow, continuous
- Techniques
- svg, interval, filter, keyframes
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A real <button> with a visible label; decoration is aria-hidden. Focus makes it live like hover. Under prefers-reduced-motion no arcs are drawn.