Waveform
React + Tailwind
const css = `
@keyframes toggle-waveform-run { to { transform: translateX(-20px); } }
.toggle-waveform { position: relative; display: inline-flex; align-items: center; gap: 12px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
.toggle-waveform__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-waveform__track {
position: relative;
width: 84px;
height: 32px;
border-radius: 999px;
background: light-dark(#f3f4f7, #1a1c1f);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
overflow: hidden;
}
.toggle-waveform__input:focus-visible + .toggle-waveform__track { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
.toggle-waveform__icon { position: absolute; left: 9px; top: 9px; width: 14px; height: 14px; color: light-dark(#5f626a, #a7abb2); transition: color 0.3s; }
.toggle-waveform__icon path:last-child { opacity: 1; transition: opacity 0.2s; }
.toggle-waveform__icon path:nth-child(2) { opacity: 0; transition: opacity 0.2s; }
.toggle-waveform__window { position: absolute; left: 26px; right: 8px; top: 0; bottom: 0; overflow: hidden; mask-image: linear-gradient(90deg, transparent, #000 25%, #000 75%, transparent); }
.toggle-waveform__amp { position: absolute; left: 0; top: 0; width: 100px; height: 32px; transform: scaleY(0.05); transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); }
.toggle-waveform__amp svg { display: block; width: 100px; height: 32px; animation: toggle-waveform-run 0.7s linear infinite paused; }
.toggle-waveform__amp path { fill: none; stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.8; vector-effect: non-scaling-stroke; transition: stroke 0.3s; }
.toggle-waveform__input:checked + .toggle-waveform__track .toggle-waveform__icon { color: light-dark(#373a40, #f1f2f3); }
.toggle-waveform__input:checked + .toggle-waveform__track .toggle-waveform__icon path:nth-child(2) { opacity: 1; }
.toggle-waveform__input:checked + .toggle-waveform__track .toggle-waveform__icon path:last-child { opacity: 0; }
.toggle-waveform__input:checked + .toggle-waveform__track .toggle-waveform__amp { transform: scaleY(1); }
.toggle-waveform__input:checked + .toggle-waveform__track .toggle-waveform__amp svg { animation-play-state: running; }
.toggle-waveform__input:checked + .toggle-waveform__track .toggle-waveform__amp path { stroke: light-dark(#373a40, #f1f2f3); }
@media (prefers-reduced-motion: reduce) {
.toggle-waveform__amp { transition-duration: 0.01s; }
.toggle-waveform__amp svg { animation: none !important; }
}
`;
// A sine with a 20px period, long enough to scroll one period without a seam.
const WAVE = (() => {
let d = '';
for (let x = 0; x <= 100; x += 2) {
const env = 0.55 + 0.45 * Math.sin((x / 100) * Math.PI * 3);
d += `${x ? 'L' : 'M'}${x} ${(16 - Math.sin((x / 20) * Math.PI * 2) * 11 * env).toFixed(2)}`;
}
return d;
})();
export function WaveformToggle() {
return (
<>
<style>
{css}
</style>
<label className="toggle-waveform">
<input className="toggle-waveform__input" type="checkbox" role="switch" />
<span className="toggle-waveform__track" aria-hidden="true">
<svg className="toggle-waveform__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 9h4l5-4v14l-5-4H4z" fill="currentColor" />
<path d="M16.5 8.5a5 5 0 0 1 0 7M19 6a8.5 8.5 0 0 1 0 12" />
<path d="M17 9.5l5 5M22 9.5l-5 5" />
</svg>
<span className="toggle-waveform__window">
<span className="toggle-waveform__amp">
<svg viewBox="0 0 100 32" preserveAspectRatio="none">
<path d={WAVE} />
</svg>
</span>
</span>
</span>
Sound
</label>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<label class="toggle-waveform"><input class="toggle-waveform__input" type="checkbox" role="switch"/><span class="toggle-waveform__track" aria-hidden="true"><svg class="toggle-waveform__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9h4l5-4v14l-5-4H4z" fill="currentColor"></path><path d="M16.5 8.5a5 5 0 0 1 0 7M19 6a8.5 8.5 0 0 1 0 12"></path><path d="M17 9.5l5 5M22 9.5l-5 5"></path></svg><span class="toggle-waveform__window"><span class="toggle-waveform__amp"><svg viewBox="0 0 100 32" preserveAspectRatio="none"><path d="M0 16.00L2 11.90L4 8.51L6 7.72L8 10.45L10 16.00L12 22.19L14 26.31L16 26.45L18 22.44L20 16.00L22 9.89L24 6.62L26 7.25L28 11.04L30 16.00L32 19.92L34 21.46L36 20.58L38 18.32L40 16.00L42 14.56L44 14.22L46 14.62L48 15.30L50 16.00L52 16.70L54 17.38L56 17.78L58 17.44L60 16.00L62 13.68L64 11.42L66 10.54L68 12.08L70 16.00L72 20.96L74 24.75L76 25.38L78 22.11L80 16.00L82 9.56L84 5.55L86 5.69L88 9.81L90 16.00L92 21.55L94 24.28L96 23.49L98 20.10L100 16.00"></path></svg></span></span></span>Sound</label>CSS
@keyframes toggle-waveform-run { to { transform: translateX(-20px); } }
.toggle-waveform { position: relative; display: inline-flex; align-items: center; gap: 12px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
.toggle-waveform__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-waveform__track {
position: relative;
width: 84px;
height: 32px;
border-radius: 999px;
background: light-dark(#f3f4f7, #1a1c1f);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
overflow: hidden;
}
.toggle-waveform__input:focus-visible + .toggle-waveform__track { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
.toggle-waveform__icon { position: absolute; left: 9px; top: 9px; width: 14px; height: 14px; color: light-dark(#5f626a, #a7abb2); transition: color 0.3s; }
.toggle-waveform__icon path:last-child { opacity: 1; transition: opacity 0.2s; }
.toggle-waveform__icon path:nth-child(2) { opacity: 0; transition: opacity 0.2s; }
.toggle-waveform__window { position: absolute; left: 26px; right: 8px; top: 0; bottom: 0; overflow: hidden; mask-image: linear-gradient(90deg, transparent, #000 25%, #000 75%, transparent); }
.toggle-waveform__amp { position: absolute; left: 0; top: 0; width: 100px; height: 32px; transform: scaleY(0.05); transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); }
.toggle-waveform__amp svg { display: block; width: 100px; height: 32px; animation: toggle-waveform-run 0.7s linear infinite paused; }
.toggle-waveform__amp path { fill: none; stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.8; vector-effect: non-scaling-stroke; transition: stroke 0.3s; }
.toggle-waveform__input:checked + .toggle-waveform__track .toggle-waveform__icon { color: light-dark(#373a40, #f1f2f3); }
.toggle-waveform__input:checked + .toggle-waveform__track .toggle-waveform__icon path:nth-child(2) { opacity: 1; }
.toggle-waveform__input:checked + .toggle-waveform__track .toggle-waveform__icon path:last-child { opacity: 0; }
.toggle-waveform__input:checked + .toggle-waveform__track .toggle-waveform__amp { transform: scaleY(1); }
.toggle-waveform__input:checked + .toggle-waveform__track .toggle-waveform__amp svg { animation-play-state: running; }
.toggle-waveform__input:checked + .toggle-waveform__track .toggle-waveform__amp path { stroke: light-dark(#373a40, #f1f2f3); }
@media (prefers-reduced-motion: reduce) {
.toggle-waveform__amp { transition-duration: 0.01s; }
.toggle-waveform__amp svg { animation: none !important; }
}Goal: Create a "Waveform" toggle.
Visual behavior: A sound switch: the speaker unmutes and a flat line springs up into a running sound wave.
Technique:
- A real native checkbox with role="switch" (visually hidden: absolute, 1x1, opacity 0) inside a <label>; every visual is aria-hidden and styled from the input with :checked + sibling selectors.
- The wave is an SVG path of several 20px periods inside a masked window; it scrolls one period on a loop that is paused off and running on.
- A wrapper scales it on Y from 0.05 (flat) to 1 on an overshooting curve, with vector-effect: non-scaling-stroke so the line keeps its width; the speaker icon swaps its cross for waves.
Accessibility: A native checkbox with role="switch" inside a <label>, so Space toggles it and screen readers announce a switch; the visible label names it and :focus-visible on the input outlines the visual. Under prefers-reduced-motion the wave stands still.
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 "Waveform" toggle.
Visual behavior: A sound switch: the speaker unmutes and a flat line springs up into a running sound wave.
Technique:
- A real native checkbox with role="switch" (visually hidden: absolute, 1x1, opacity 0) inside a <label>; every visual is aria-hidden and styled from the input with :checked + sibling selectors.
- The wave is an SVG path of several 20px periods inside a masked window; it scrolls one period on a loop that is paused off and running on.
- A wrapper scales it on Y from 0.05 (flat) to 1 on an overshooting curve, with vector-effect: non-scaling-stroke so the line keeps its width; the speaker icon swaps its cross for waves.
Accessibility: A native checkbox with role="switch" inside a <label>, so Space toggles it and screen readers announce a switch; the visible label names it and :focus-visible on the input outlines the visual. Under prefers-reduced-motion the wave stands still.
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 sound switch: the speaker unmutes and a flat line springs up into a running sound wave.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- click, state change, continuous, spring
- Techniques
- svg, keyframes, css transition
- 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 screen readers announce a switch; the visible label names it and :focus-visible on the input outlines the visual. Under prefers-reduced-motion the wave stands still.