Burner
import type { CSSProperties } from 'react';
const css = `
@keyframes toggle-burner-flicker { 0%, 100% { transform: scale(1, 1); } 30% { transform: scale(0.92, 1.12); } 60% { transform: scale(1.05, 0.9); } }
@keyframes toggle-burner-spark { 0%, 20%, 45%, 100% { opacity: 0; transform: scale(0.4); } 8%, 30% { opacity: 1; transform: scale(1); } }
.toggle-burner { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
.toggle-burner__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-burner__art { display: block; width: 124px; height: 90px; overflow: visible; border-radius: 8px; }
.toggle-burner__input:focus-visible + .toggle-burner__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
.toggle-burner__top { fill: light-dark(#dbdde1, #2e3035); }
.toggle-burner__front { fill: light-dark(#ebedf0, #232529); }
.toggle-burner__base { fill: light-dark(#cbced3, #393c41); }
.toggle-burner__cap { fill: light-dark(#6d7179, #7d8087); }
.toggle-burner__glow { fill: light-dark(#373a40, #f1f2f3); opacity: 0; filter: blur(6px); transition: opacity 0.4s; }
.toggle-burner__knob { fill: light-dark(#6d7179, #7d8087); }
.toggle-burner__mark { stroke: light-dark(#373a40, #f1f2f3); stroke-width: 1.6; stroke-linecap: round; }
.toggle-burner__dial { transform-box: view-box; transform-origin: 80px 62px; transition: transform 0.3s cubic-bezier(0.3, 1.3, 0.5, 1); }
.toggle-burner__spark { fill: light-dark(#373a40, #f1f2f3); opacity: 0; transform-box: fill-box; transform-origin: 50% 50%; }
/* Each flame grows from its port. They catch one after another round the ring once the spark has fired. */
.toggle-burner__flame { transform-box: fill-box; transform-origin: 50% 100%; transform: scale(0.2, 0); transition: transform 0.18s ease-in; }
.toggle-burner__tongue { fill: light-dark(#42454c, #e5e6e9); transform-box: fill-box; transform-origin: 50% 100%; animation: toggle-burner-flicker var(--t) ease-in-out infinite; }
.toggle-burner__tongue--back { fill: light-dark(#5f626a, #a7abb2); }
.toggle-burner__input:checked + .toggle-burner__art .toggle-burner__dial { transform: rotate(-90deg); }
.toggle-burner__input:checked + .toggle-burner__art .toggle-burner__spark { animation: toggle-burner-spark 0.5s 0.1s; }
.toggle-burner__input:checked + .toggle-burner__art .toggle-burner__flame { transform: none; transition: transform 0.3s cubic-bezier(0.3, 1.5, 0.5, 1) calc(0.4s + var(--i) * 30ms); }
.toggle-burner__input:checked + .toggle-burner__art .toggle-burner__glow { opacity: 0.18; transition-delay: 0.5s; }
@media (prefers-reduced-motion: reduce) {
.toggle-burner__tongue { animation: none; }
.toggle-burner__dial, .toggle-burner__flame, .toggle-burner__input:checked + .toggle-burner__art .toggle-burner__flame { transition: none; }
.toggle-burner__input:checked + .toggle-burner__art .toggle-burner__spark { animation: none; }
}
`;
// Twelve ports round the burner, seen from the front: the back half sits behind the cap and reads smaller.
const PORTS = Array.from({ length: 12 }, (_, i) => {
const a = (i / 12) * Math.PI * 2 + Math.PI / 2;
return { i, x: 40 + 21 * Math.cos(a), y: 45 + 5.5 * Math.sin(a), back: Math.sin(a) < 0, t: 0.5 + ((i * 7) % 5) * 0.08 };
});
const order = (i: number) => (i <= 6 ? i : 12 - i);
function Flame({ p }: { p: (typeof PORTS)[number] }) {
const s = p.back ? 0.72 : 1;
return (
<g transform={`translate(${p.x.toFixed(2)} ${p.y.toFixed(2)}) scale(${s})`}>
<g className="toggle-burner__flame" style={{ '--i': order(p.i) } as CSSProperties}>
<path
className={`toggle-burner__tongue${p.back ? ' toggle-burner__tongue--back' : ''}`}
style={{ '--t': `${p.t}s` } as CSSProperties}
d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"
/>
</g>
</g>
);
}
export function BurnerToggle() {
return (
<>
<style>
{css}
</style>
<label className="toggle-burner">
<input className="toggle-burner__input" type="checkbox" role="switch" />
<svg className="toggle-burner__art" viewBox="0 0 96 70" aria-hidden="true">
<ellipse className="toggle-burner__glow" cx="40" cy="38" rx="28" ry="12" />
<rect className="toggle-burner__top" x="2" y="50" width="92" height="5" rx="2" />
<rect className="toggle-burner__front" x="4" y="55" width="88" height="14" rx="2" />
<ellipse className="toggle-burner__base" cx="40" cy="48" rx="25" ry="5" />
{PORTS.filter((p) => p.back).map((p) => (
<Flame key={p.i} p={p} />
))}
<ellipse className="toggle-burner__cap" cx="40" cy="44" rx="17" ry="4.5" />
{PORTS.filter((p) => !p.back).map((p) => (
<Flame key={p.i} p={p} />
))}
<path className="toggle-burner__spark" d="M64 40L66 43L69 42L67 45L69 48L66 47L64 50L63 47L60 47L62 45L60 42L63 43Z" />
<g className="toggle-burner__dial">
<circle className="toggle-burner__knob" cx="80" cy="62" r="5.5" />
<line className="toggle-burner__mark" x1="80" y1="62" x2="80" y2="57.5" />
</g>
</svg>
<span>Heat</span>
</label>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<label class="toggle-burner">
<input class="toggle-burner__input" type="checkbox" role="switch"/>
<svg class="toggle-burner__art" viewBox="0 0 96 70" aria-hidden="true">
<ellipse class="toggle-burner__glow" cx="40" cy="38" rx="28" ry="12"></ellipse>
<rect class="toggle-burner__top" x="2" y="50" width="92" height="5" rx="2"></rect>
<rect class="toggle-burner__front" x="4" y="55" width="88" height="14" rx="2"></rect>
<ellipse class="toggle-burner__base" cx="40" cy="48" rx="25" ry="5"></ellipse>
<g transform="translate(21.81 42.25) scale(0.72)">
<g class="toggle-burner__flame" style="--i:4">
<path class="toggle-burner__tongue toggle-burner__tongue--back" style="--t:0.74s" d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"></path>
</g>
</g>
<g transform="translate(29.50 40.24) scale(0.72)">
<g class="toggle-burner__flame" style="--i:5">
<path class="toggle-burner__tongue toggle-burner__tongue--back" style="--t:0.5s" d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"></path>
</g>
</g>
<g transform="translate(40.00 39.50) scale(0.72)">
<g class="toggle-burner__flame" style="--i:6">
<path class="toggle-burner__tongue toggle-burner__tongue--back" style="--t:0.66s" d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"></path>
</g>
</g>
<g transform="translate(50.50 40.24) scale(0.72)">
<g class="toggle-burner__flame" style="--i:5">
<path class="toggle-burner__tongue toggle-burner__tongue--back" style="--t:0.82s" d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"></path>
</g>
</g>
<g transform="translate(58.19 42.25) scale(0.72)">
<g class="toggle-burner__flame" style="--i:4">
<path class="toggle-burner__tongue toggle-burner__tongue--back" style="--t:0.58s" d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"></path>
</g>
</g>
<g transform="translate(61.00 45.00) scale(0.72)">
<g class="toggle-burner__flame" style="--i:3">
<path class="toggle-burner__tongue toggle-burner__tongue--back" style="--t:0.74s" d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"></path>
</g>
</g>
<ellipse class="toggle-burner__cap" cx="40" cy="44" rx="17" ry="4.5"></ellipse>
<g transform="translate(40.00 50.50) scale(1)">
<g class="toggle-burner__flame" style="--i:0">
<path class="toggle-burner__tongue" style="--t:0.5s" d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"></path>
</g>
</g>
<g transform="translate(29.50 49.76) scale(1)">
<g class="toggle-burner__flame" style="--i:1">
<path class="toggle-burner__tongue" style="--t:0.66s" d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"></path>
</g>
</g>
<g transform="translate(21.81 47.75) scale(1)">
<g class="toggle-burner__flame" style="--i:2">
<path class="toggle-burner__tongue" style="--t:0.82s" d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"></path>
</g>
</g>
<g transform="translate(19.00 45.00) scale(1)">
<g class="toggle-burner__flame" style="--i:3">
<path class="toggle-burner__tongue" style="--t:0.58s" d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"></path>
</g>
</g>
<g transform="translate(58.19 47.75) scale(1)">
<g class="toggle-burner__flame" style="--i:2">
<path class="toggle-burner__tongue" style="--t:0.5s" d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"></path>
</g>
</g>
<g transform="translate(50.50 49.76) scale(1)">
<g class="toggle-burner__flame" style="--i:1">
<path class="toggle-burner__tongue" style="--t:0.66s" d="M0 0C-2.6-2-2.4-6 0-13C2.4-6 2.6-2 0 0Z"></path>
</g>
</g>
<path class="toggle-burner__spark" d="M64 40L66 43L69 42L67 45L69 48L66 47L64 50L63 47L60 47L62 45L60 42L63 43Z"></path>
<g class="toggle-burner__dial">
<circle class="toggle-burner__knob" cx="80" cy="62" r="5.5"></circle>
<line class="toggle-burner__mark" x1="80" y1="62" x2="80" y2="57.5"></line>
</g>
</svg>
<span>Heat</span>
</label>@keyframes toggle-burner-flicker { 0%, 100% { transform: scale(1, 1); } 30% { transform: scale(0.92, 1.12); } 60% { transform: scale(1.05, 0.9); } }
@keyframes toggle-burner-spark { 0%, 20%, 45%, 100% { opacity: 0; transform: scale(0.4); } 8%, 30% { opacity: 1; transform: scale(1); } }
.toggle-burner { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
.toggle-burner__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-burner__art { display: block; width: 124px; height: 90px; overflow: visible; border-radius: 8px; }
.toggle-burner__input:focus-visible + .toggle-burner__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
.toggle-burner__top { fill: light-dark(#dbdde1, #2e3035); }
.toggle-burner__front { fill: light-dark(#ebedf0, #232529); }
.toggle-burner__base { fill: light-dark(#cbced3, #393c41); }
.toggle-burner__cap { fill: light-dark(#6d7179, #7d8087); }
.toggle-burner__glow { fill: light-dark(#373a40, #f1f2f3); opacity: 0; filter: blur(6px); transition: opacity 0.4s; }
.toggle-burner__knob { fill: light-dark(#6d7179, #7d8087); }
.toggle-burner__mark { stroke: light-dark(#373a40, #f1f2f3); stroke-width: 1.6; stroke-linecap: round; }
.toggle-burner__dial { transform-box: view-box; transform-origin: 80px 62px; transition: transform 0.3s cubic-bezier(0.3, 1.3, 0.5, 1); }
.toggle-burner__spark { fill: light-dark(#373a40, #f1f2f3); opacity: 0; transform-box: fill-box; transform-origin: 50% 50%; }
/* Each flame grows from its port. They catch one after another round the ring once the spark has fired. */
.toggle-burner__flame { transform-box: fill-box; transform-origin: 50% 100%; transform: scale(0.2, 0); transition: transform 0.18s ease-in; }
.toggle-burner__tongue { fill: light-dark(#42454c, #e5e6e9); transform-box: fill-box; transform-origin: 50% 100%; animation: toggle-burner-flicker var(--t) ease-in-out infinite; }
.toggle-burner__tongue--back { fill: light-dark(#5f626a, #a7abb2); }
.toggle-burner__input:checked + .toggle-burner__art .toggle-burner__dial { transform: rotate(-90deg); }
.toggle-burner__input:checked + .toggle-burner__art .toggle-burner__spark { animation: toggle-burner-spark 0.5s 0.1s; }
.toggle-burner__input:checked + .toggle-burner__art .toggle-burner__flame { transform: none; transition: transform 0.3s cubic-bezier(0.3, 1.5, 0.5, 1) calc(0.4s + var(--i) * 30ms); }
.toggle-burner__input:checked + .toggle-burner__art .toggle-burner__glow { opacity: 0.18; transition-delay: 0.5s; }
@media (prefers-reduced-motion: reduce) {
.toggle-burner__tongue { animation: none; }
.toggle-burner__dial, .toggle-burner__flame, .toggle-burner__input:checked + .toggle-burner__art .toggle-burner__flame { transition: none; }
.toggle-burner__input:checked + .toggle-burner__art .toggle-burner__spark { animation: none; }
}Goal: Create a "Burner" toggle switch.
Visual behavior: A heat switch drawn as a gas hob: the knob turns, the igniter sparks and a ring of flames catches round the burner one after another, then flickers.
Technique:
- A native checkbox with role="switch", visually hidden but focusable, sits inside a <label> before an aria-hidden drawing and the visible text. Every on state is styled from the input's :checked state through the adjacent sibling combinator, so Space and clicks both work.
- Twelve flames sit on an ellipse of ports, the back ones smaller behind the cap. Each grows from its port with a delay set by its place round the ring once the spark keyframe has fired, then loops a flicker.
- The knob turns a quarter with the checked state. Pure CSS.
Accessibility: A native checkbox with role="switch" inside a <label>, so Space toggles it and the visible text names it; :focus-visible on the input outlines the drawing, which is aria-hidden. Under prefers-reduced-motion the flames light without catching round or flickering.
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 "Burner" toggle switch.
Visual behavior: A heat switch drawn as a gas hob: the knob turns, the igniter sparks and a ring of flames catches round the burner one after another, then flickers.
Technique:
- A native checkbox with role="switch", visually hidden but focusable, sits inside a <label> before an aria-hidden drawing and the visible text. Every on state is styled from the input's :checked state through the adjacent sibling combinator, so Space and clicks both work.
- Twelve flames sit on an ellipse of ports, the back ones smaller behind the cap. Each grows from its port with a delay set by its place round the ring once the spark keyframe has fired, then loops a flicker.
- The knob turns a quarter with the checked state. Pure CSS.
Accessibility: A native checkbox with role="switch" inside a <label>, so Space toggles it and the visible text names it; :focus-visible on the input outlines the drawing, which is aria-hidden. Under prefers-reduced-motion the flames light without catching round or flickering.
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 heat switch drawn as a gas hob: the knob turns, the igniter sparks and a ring of flames catches round the burner one after another, then flickers.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, delay stagger, loop
- Techniques
- keyframes, stagger, svg
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native checkbox with role="switch" inside a <label>, so Space toggles it and the visible text names it; :focus-visible on the input outlines the drawing, which is aria-hidden. Under prefers-reduced-motion the flames light without catching round or flickering.