Plasma
import { useEffect, useId, useRef } from 'react';
const css = `
.btn-plasma {
position: relative;
display: grid;
place-items: center;
width: 200px;
height: 58px;
padding: 0;
border: 1px solid light-dark(#cbced3, #393c41);
border-radius: 999px;
background: light-dark(#fbfcfd, #121417);
color: light-dark(#373a40, #f1f2f3);
font-family: inherit;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
cursor: pointer;
overflow: hidden;
transition: border-color 0.4s ease, box-shadow 0.4s ease;
}
.btn-plasma:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.btn-plasma__energy {
position: absolute;
inset: -10px;
width: calc(100% + 20px);
height: calc(100% + 20px);
color: light-dark(#373a40, #f1f2f3);
opacity: 0.32;
transition: opacity 0.5s ease;
}
.btn-plasma__label {
position: relative;
text-shadow: 0 0 6px light-dark(#fbfcfd, #121417), 0 0 14px light-dark(#fbfcfd, #121417);
}
.btn-plasma:hover, .btn-plasma:focus-visible {
border-color: light-dark(#50535b, #c6c8cd);
box-shadow: 0 0 28px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 26%, transparent);
}
.btn-plasma:hover .btn-plasma__energy,
.btn-plasma:focus-visible .btn-plasma__energy { opacity: 0.95; }
`;
export function PlasmaButton() {
const id = `plasma-${useId().replace(/:/g, '')}`;
const svg = useRef<SVGSVGElement>(null);
// SMIL ignores the CSS media query, so reduced motion pauses it here.
useEffect(() => {
if (matchMedia('(prefers-reduced-motion: reduce)').matches) svg.current?.pauseAnimations();
}, []);
return (
<>
<style>
{css}
</style>
<button type="button" className="btn-plasma">
<svg ref={svg} className="btn-plasma__energy" aria-hidden="true">
<filter id={id} x="0" y="0" width="100%" height="100%">
{/* Churning noise: the frequency itself is animated, so the field never repeats exactly. */}
<feTurbulence type="turbulence" baseFrequency="0.012 0.06" numOctaves={4} seed={4} result="noise">
<animate
attributeName="baseFrequency"
dur="9s"
values="0.012 0.06; 0.02 0.08; 0.009 0.05; 0.012 0.06"
repeatCount="indefinite"
/>
</feTurbulence>
{/* Turbulence is bright along its ridges: keep those as alpha, softly. */}
<feColorMatrix in="noise" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3.2 0 0 0 -0.55" result="mask" />
{/* Paint those cores in the text colour. */}
<feFlood floodColor="currentColor" result="ink" />
<feComposite in="ink" in2="mask" operator="in" />
</filter>
<rect width="100%" height="100%" filter={`url(#${id})`} fill="currentColor" />
</svg>
<span className="btn-plasma__label">Ignite</span>
</button>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initBtnPlasma(el) on the mounted element instead.
<button type="button" class="btn-plasma">
<svg class="btn-plasma__energy" aria-hidden="true">
<filter id="plasma-_R_0_" x="0" y="0" width="100%" height="100%">
<feTurbulence type="turbulence" baseFrequency="0.012 0.06" numOctaves="4" seed="4" result="noise">
<animate attributeName="baseFrequency" dur="9s" values="0.012 0.06; 0.02 0.08; 0.009 0.05; 0.012 0.06" repeatCount="indefinite"></animate>
</feTurbulence>
<feColorMatrix in="noise" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3.2 0 0 0 -0.55" result="mask"></feColorMatrix>
<feFlood flood-color="currentColor" result="ink"></feFlood>
<feComposite in="ink" in2="mask" operator="in"></feComposite>
</filter>
<rect width="100%" height="100%" filter="url(#plasma-_R_0_)" fill="currentColor"></rect>
</svg>
<span class="btn-plasma__label">Ignite</span>
</button>.btn-plasma {
position: relative;
display: grid;
place-items: center;
width: 200px;
height: 58px;
padding: 0;
border: 1px solid light-dark(#cbced3, #393c41);
border-radius: 999px;
background: light-dark(#fbfcfd, #121417);
color: light-dark(#373a40, #f1f2f3);
font-family: inherit;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
cursor: pointer;
overflow: hidden;
transition: border-color 0.4s ease, box-shadow 0.4s ease;
}
.btn-plasma:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.btn-plasma__energy {
position: absolute;
inset: -10px;
width: calc(100% + 20px);
height: calc(100% + 20px);
color: light-dark(#373a40, #f1f2f3);
opacity: 0.32;
transition: opacity 0.5s ease;
}
.btn-plasma__label {
position: relative;
text-shadow: 0 0 6px light-dark(#fbfcfd, #121417), 0 0 14px light-dark(#fbfcfd, #121417);
}
.btn-plasma:hover, .btn-plasma:focus-visible {
border-color: light-dark(#50535b, #c6c8cd);
box-shadow: 0 0 28px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 26%, transparent);
}
.btn-plasma:hover .btn-plasma__energy,
.btn-plasma:focus-visible .btn-plasma__energy { opacity: 0.95; }var btnPlasmaCount = 0;
function initBtnPlasma(root) {
var svg = root.querySelector('.btn-plasma__energy');
if (!svg) return;
// The filter id must be unique per copy on the page.
var id = 'btn-plasma-' + ++btnPlasmaCount;
svg.querySelector('filter').id = id;
svg.querySelector('rect').setAttribute('filter', 'url(#' + id + ')');
// SMIL ignores the CSS media query, so reduced motion pauses it here.
if (matchMedia('(prefers-reduced-motion: reduce)').matches) svg.pauseAnimations();
}
document.querySelectorAll('.btn-plasma').forEach(initBtnPlasma);Goal: Create a "Plasma" button.
Visual behavior: Live turbulent energy churns inside a pill button: bright filaments of plasma that never repeat. At rest they smoulder faintly; hover turns them up to a blaze behind the label, with a glow round the rim.
Technique:
- An SVG rect behind the label is filtered: feTurbulence (type turbulence, 4 octaves) whose baseFrequency is animated by SMIL over 9s, so the field keeps churning and never loops visibly.
- feColorMatrix turns the noise's bright ridges into alpha (3.2 x red - 0.55), keeping only the cores of the filaments.
- feFlood with currentColor and feComposite operator="in" paint those cores in the text colour, so the plasma follows the theme.
- Hover and focus raise the energy layer from 0.32 to 0.95 opacity and add an outer glow. SMIL ignores CSS media queries, so reduced motion calls pauseAnimations() on the SVG.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. Focus blazes like hover. Under prefers-reduced-motion the SVG animation is paused.
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 "Plasma" button.
Visual behavior: Live turbulent energy churns inside a pill button: bright filaments of plasma that never repeat. At rest they smoulder faintly; hover turns them up to a blaze behind the label, with a glow round the rim.
Technique:
- An SVG rect behind the label is filtered: feTurbulence (type turbulence, 4 octaves) whose baseFrequency is animated by SMIL over 9s, so the field keeps churning and never loops visibly.
- feColorMatrix turns the noise's bright ridges into alpha (3.2 x red - 0.55), keeping only the cores of the filaments.
- feFlood with currentColor and feComposite operator="in" paint those cores in the text colour, so the plasma follows the theme.
- Hover and focus raise the energy layer from 0.32 to 0.95 opacity and add an outer glow. SMIL ignores CSS media queries, so reduced motion calls pauseAnimations() on the SVG.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. Focus blazes like hover. Under prefers-reduced-motion the SVG animation is paused.
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
Live turbulent energy churns inside a pill button: bright filaments of plasma that never repeat. At rest they smoulder faintly; hover turns them up to a blaze behind the label, with a glow round the rim.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- hover, glow, continuous, ambient
- Techniques
- svg filter, filter, css transition
- 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 blazes like hover. Under prefers-reduced-motion the SVG animation is paused.