Kettle
import type { CSSProperties } from 'react';
const css = `
@keyframes toggle-kettle-rise {
0% { opacity: 0; transform: translate(0, 0); }
15% { opacity: 1; }
100% { opacity: 0; transform: translate(var(--x), -15px); }
}
@keyframes toggle-kettle-steam {
0% { opacity: 0; transform: translate(0, 0) scale(0.6); }
30% { opacity: 0.8; }
100% { opacity: 0; transform: translate(-3px, -14px) scale(1.25); }
}
@keyframes toggle-kettle-boil { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-0.8px); } }
.toggle-kettle { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
.toggle-kettle__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-kettle__art { display: block; width: 92px; height: 90px; overflow: visible; border-radius: 8px; }
.toggle-kettle__input:focus-visible + .toggle-kettle__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
.toggle-kettle__base { fill: light-dark(#dbdde1, #2e3035); }
.toggle-kettle__body { fill: light-dark(#dbdde1, #2e3035); }
.toggle-kettle__shine { fill: light-dark(#cbced3, #393c41); }
.toggle-kettle__lid { fill: light-dark(#cbced3, #393c41); }
.toggle-kettle__handle { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 5; stroke-linecap: round; }
.toggle-kettle__window { fill: light-dark(#f3f4f7, #1a1c1f); }
.toggle-kettle__water { fill: light-dark(#6d7179, #7d8087); }
.toggle-kettle__bubble { fill: light-dark(#42454c, #e5e6e9); opacity: 0; transform-box: fill-box; }
.toggle-kettle__steam { fill: none; stroke: light-dark(#5f626a, #a7abb2); stroke-width: 2; stroke-linecap: round; opacity: 0; transform-box: fill-box; transform-origin: 50% 100%; }
.toggle-kettle__lever { fill: light-dark(#5f626a, #a7abb2); transform-box: fill-box; transform-origin: 0 50%; transition: transform 0.25s cubic-bezier(0.3, 1.6, 0.5, 1); }
.toggle-kettle__led { fill: light-dark(#cbced3, #393c41); transition: fill 0.2s, filter 0.2s; }
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__lever { transform: rotate(-30deg); }
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__led { fill: light-dark(#373a40, #f1f2f3); filter: drop-shadow(0 0 2.5px light-dark(#373a40, #f1f2f3)); }
/* The water heats for a moment before it bubbles, and steams a little after that. */
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__bubble { animation: toggle-kettle-rise 1.1s ease-in calc(0.5s + var(--d)) infinite; }
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__surface { animation: toggle-kettle-boil 0.3s steps(2) 0.9s infinite; }
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__steam { animation: toggle-kettle-steam 1.8s ease-out calc(1.3s + var(--d)) infinite; }
@media (prefers-reduced-motion: reduce) {
.toggle-kettle__lever { transition: none; }
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__bubble,
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__surface { animation: none; }
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__steam { animation: none; opacity: 0.6; }
}
`;
const BUBBLES = [
{ x: 33.5, r: 1.2, d: 0, dx: 0.6 },
{ x: 36, r: 1.6, d: 0.35, dx: -0.5 },
{ x: 38.5, r: 1.1, d: 0.7, dx: 0.4 },
{ x: 35, r: 1, d: 0.95, dx: 0.8 },
{ x: 37.5, r: 1.3, d: 0.2, dx: -0.7 },
];
export function KettleToggle() {
return (
<>
<style>
{css}
</style>
<label className="toggle-kettle">
<input className="toggle-kettle__input" type="checkbox" role="switch" />
<svg className="toggle-kettle__art" viewBox="0 0 76 74" aria-hidden="true">
{[0, 0.6, 1.2].map((d, i) => (
<path key={d} className="toggle-kettle__steam" style={{ '--d': `${d}s` } as CSSProperties} d={`M${7 + i * 3} 22c-3-3 3-5 0-8`} />
))}
<path className="toggle-kettle__handle" d="M50 24C62 23 64 30 64 40C64 50 60 56 53 57" />
<path className="toggle-kettle__body" d="M20 40L8 27Q6 25 9 24H12L22 33Z" />
<path className="toggle-kettle__body" d="M18 62L21 24Q22 18 28 18H44Q50 18 51 24L54 62Z" />
<path className="toggle-kettle__shine" d="M24 24Q25 21 28 21L26 56H22Z" />
<path className="toggle-kettle__lid" d="M25 18Q36 10 47 18Z" />
<rect className="toggle-kettle__lid" x="33" y="9" width="6" height="3" rx="1.5" />
<rect className="toggle-kettle__window" x="31" y="27" width="10" height="29" rx="3" />
<g className="toggle-kettle__surface">
<path className="toggle-kettle__water" d="M31 39H41V53A3 3 0 0 1 38 56H34A3 3 0 0 1 31 53Z" />
</g>
{BUBBLES.map((b) => (
<circle key={b.x} className="toggle-kettle__bubble" cx={b.x} cy="54" r={b.r} style={{ '--d': `${b.d}s`, '--x': `${b.dx}px` } as CSSProperties} />
))}
<rect className="toggle-kettle__lever" x="55" y="54" width="8" height="3.5" rx="1.5" />
<rect className="toggle-kettle__base" x="12" y="62" width="48" height="8" rx="3" />
<circle className="toggle-kettle__led" cx="51" cy="66" r="1.8" />
</svg>
<span>Kettle</span>
</label>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<label class="toggle-kettle">
<input class="toggle-kettle__input" type="checkbox" role="switch"/>
<svg class="toggle-kettle__art" viewBox="0 0 76 74" aria-hidden="true">
<path class="toggle-kettle__steam" style="--d:0s" d="M7 22c-3-3 3-5 0-8"></path>
<path class="toggle-kettle__steam" style="--d:0.6s" d="M10 22c-3-3 3-5 0-8"></path>
<path class="toggle-kettle__steam" style="--d:1.2s" d="M13 22c-3-3 3-5 0-8"></path>
<path class="toggle-kettle__handle" d="M50 24C62 23 64 30 64 40C64 50 60 56 53 57"></path>
<path class="toggle-kettle__body" d="M20 40L8 27Q6 25 9 24H12L22 33Z"></path>
<path class="toggle-kettle__body" d="M18 62L21 24Q22 18 28 18H44Q50 18 51 24L54 62Z"></path>
<path class="toggle-kettle__shine" d="M24 24Q25 21 28 21L26 56H22Z"></path>
<path class="toggle-kettle__lid" d="M25 18Q36 10 47 18Z"></path>
<rect class="toggle-kettle__lid" x="33" y="9" width="6" height="3" rx="1.5"></rect>
<rect class="toggle-kettle__window" x="31" y="27" width="10" height="29" rx="3"></rect>
<g class="toggle-kettle__surface">
<path class="toggle-kettle__water" d="M31 39H41V53A3 3 0 0 1 38 56H34A3 3 0 0 1 31 53Z"></path>
</g>
<circle class="toggle-kettle__bubble" cx="33.5" cy="54" r="1.2" style="--d:0s;--x:0.6px"></circle>
<circle class="toggle-kettle__bubble" cx="36" cy="54" r="1.6" style="--d:0.35s;--x:-0.5px"></circle>
<circle class="toggle-kettle__bubble" cx="38.5" cy="54" r="1.1" style="--d:0.7s;--x:0.4px"></circle>
<circle class="toggle-kettle__bubble" cx="35" cy="54" r="1" style="--d:0.95s;--x:0.8px"></circle>
<circle class="toggle-kettle__bubble" cx="37.5" cy="54" r="1.3" style="--d:0.2s;--x:-0.7px"></circle>
<rect class="toggle-kettle__lever" x="55" y="54" width="8" height="3.5" rx="1.5"></rect>
<rect class="toggle-kettle__base" x="12" y="62" width="48" height="8" rx="3"></rect>
<circle class="toggle-kettle__led" cx="51" cy="66" r="1.8"></circle>
</svg>
<span>Kettle</span>
</label>@keyframes toggle-kettle-rise {
0% { opacity: 0; transform: translate(0, 0); }
15% { opacity: 1; }
100% { opacity: 0; transform: translate(var(--x), -15px); }
}
@keyframes toggle-kettle-steam {
0% { opacity: 0; transform: translate(0, 0) scale(0.6); }
30% { opacity: 0.8; }
100% { opacity: 0; transform: translate(-3px, -14px) scale(1.25); }
}
@keyframes toggle-kettle-boil { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-0.8px); } }
.toggle-kettle { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
.toggle-kettle__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-kettle__art { display: block; width: 92px; height: 90px; overflow: visible; border-radius: 8px; }
.toggle-kettle__input:focus-visible + .toggle-kettle__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
.toggle-kettle__base { fill: light-dark(#dbdde1, #2e3035); }
.toggle-kettle__body { fill: light-dark(#dbdde1, #2e3035); }
.toggle-kettle__shine { fill: light-dark(#cbced3, #393c41); }
.toggle-kettle__lid { fill: light-dark(#cbced3, #393c41); }
.toggle-kettle__handle { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 5; stroke-linecap: round; }
.toggle-kettle__window { fill: light-dark(#f3f4f7, #1a1c1f); }
.toggle-kettle__water { fill: light-dark(#6d7179, #7d8087); }
.toggle-kettle__bubble { fill: light-dark(#42454c, #e5e6e9); opacity: 0; transform-box: fill-box; }
.toggle-kettle__steam { fill: none; stroke: light-dark(#5f626a, #a7abb2); stroke-width: 2; stroke-linecap: round; opacity: 0; transform-box: fill-box; transform-origin: 50% 100%; }
.toggle-kettle__lever { fill: light-dark(#5f626a, #a7abb2); transform-box: fill-box; transform-origin: 0 50%; transition: transform 0.25s cubic-bezier(0.3, 1.6, 0.5, 1); }
.toggle-kettle__led { fill: light-dark(#cbced3, #393c41); transition: fill 0.2s, filter 0.2s; }
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__lever { transform: rotate(-30deg); }
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__led { fill: light-dark(#373a40, #f1f2f3); filter: drop-shadow(0 0 2.5px light-dark(#373a40, #f1f2f3)); }
/* The water heats for a moment before it bubbles, and steams a little after that. */
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__bubble { animation: toggle-kettle-rise 1.1s ease-in calc(0.5s + var(--d)) infinite; }
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__surface { animation: toggle-kettle-boil 0.3s steps(2) 0.9s infinite; }
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__steam { animation: toggle-kettle-steam 1.8s ease-out calc(1.3s + var(--d)) infinite; }
@media (prefers-reduced-motion: reduce) {
.toggle-kettle__lever { transition: none; }
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__bubble,
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__surface { animation: none; }
.toggle-kettle__input:checked + .toggle-kettle__art .toggle-kettle__steam { animation: none; opacity: 0.6; }
}Goal: Create a "Kettle" toggle switch.
Visual behavior: A kettle switch: the lever flips, the base light comes on, bubbles start rising in the water window and steam curls from the spout. Pure CSS.
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.
- The lever and light are transitions on the checked state. The bubbles and steam are looping keyframes with their own delays, so the water heats for a moment before it bubbles and steams a little after that.
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 it switches without the bubbles and steam moving.
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 "Kettle" toggle switch.
Visual behavior: A kettle switch: the lever flips, the base light comes on, bubbles start rising in the water window and steam curls from the spout. Pure CSS.
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.
- The lever and light are transitions on the checked state. The bubbles and steam are looping keyframes with their own delays, so the water heats for a moment before it bubbles and steams a little after that.
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 it switches without the bubbles and steam moving.
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 kettle switch: the lever flips, the base light comes on, bubbles start rising in the water window and steam curls from the spout. Pure CSS.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, loop
- Techniques
- keyframes, css transition, 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 it switches without the bubbles and steam moving.