84° field of view46° field of view28° field of view
EQ
React + Tailwind
import { useId } from 'react';
const OPTIONS = [{"value": "warm", "label": "Warm"}, {"value": "flat", "label": "Flat"}, {"value": "bright", "label": "Bright"}];
const css = `
.radio-eq { --i: 1; display: grid; justify-items: center; gap: 12px; width: 260px; }
.radio-eq:has(.radio-eq__opt:nth-child(1) input:checked) { --i: 0; }
.radio-eq:has(.radio-eq__opt:nth-child(2) input:checked) { --i: 1; }
.radio-eq:has(.radio-eq__opt:nth-child(3) input:checked) { --i: 2; }
.radio-eq__graph { position: relative; width: 220px; }
.radio-eq__graph svg { display: block; width: 220px; height: 80px; overflow: visible; }
.radio-eq__grid { stroke: light-dark(#ebedf0, #232529); stroke-width: 1; }
.radio-eq__mid { stroke: light-dark(#dbdde1, #2e3035); stroke-dasharray: 2 3; }
.radio-eq__area { fill: color-mix(in oklab, light-dark(#50535b, #c6c8cd) 16%, transparent); transition: d 0.6s cubic-bezier(0.3, 1.2, 0.5, 1); }
.radio-eq__line { fill: none; stroke: light-dark(#373a40, #f1f2f3); stroke-width: 2; stroke-linecap: round; transition: d 0.6s cubic-bezier(0.3, 1.2, 0.5, 1); }
.radio-eq:has(.radio-eq__opt:nth-child(1) input:checked) .radio-eq__line { d: path('M0 22C40 22 70 32 110 42S180 56 220 56'); }
.radio-eq:has(.radio-eq__opt:nth-child(1) input:checked) .radio-eq__area { d: path('M0 22C40 22 70 32 110 42S180 56 220 56L220 80L0 80Z'); }
.radio-eq:has(.radio-eq__opt:nth-child(2) input:checked) .radio-eq__line { d: path('M0 40C40 40 70 40 110 40S180 40 220 40'); }
.radio-eq:has(.radio-eq__opt:nth-child(2) input:checked) .radio-eq__area { d: path('M0 40C40 40 70 40 110 40S180 40 220 40L220 80L0 80Z'); }
.radio-eq:has(.radio-eq__opt:nth-child(3) input:checked) .radio-eq__line { d: path('M0 56C40 56 70 50 110 40S180 22 220 20'); }
.radio-eq:has(.radio-eq__opt:nth-child(3) input:checked) .radio-eq__area { d: path('M0 56C40 56 70 50 110 40S180 22 220 20L220 80L0 80Z'); }
.radio-eq__axis { display: flex; justify-content: space-between; margin-top: 4px; color: light-dark(#5f626a, #a7abb2); font-size: 10px; }
@media (prefers-reduced-motion: reduce) {
.radio-eq__area, .radio-eq__line { transition: none; }
}
.radio-eq__opts { display: flex; gap: 3px; padding: 4px; border-radius: 12px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.radio-eq__opt { position: relative; flex: 1; padding: 7px 10px; border-radius: 8px; color: light-dark(#5f626a, #a7abb2); font-size: 12.5px; font-weight: 500; text-align: center; white-space: nowrap; cursor: pointer; transition: background-color 0.25s, color 0.25s; }
.radio-eq__opt:hover { color: light-dark(#42454c, #e5e6e9); }
.radio-eq__opt:has(input:checked) { background: light-dark(#373a40, #f1f2f3); color: light-dark(#fbfcfd, #121417); }
.radio-eq__opt:has(input:focus-visible) { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
.radio-eq__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
`;
export function EqRadio() {
const name = useId();
return (
<>
<style>
{css}
</style>
<div className="radio-eq">
<div className="radio-eq__graph" aria-hidden="true">
<svg viewBox="0 0 220 80">
{[0, 55, 110, 165, 220].map((x) => (
<line key={x} className="radio-eq__grid" x1={x} y1="0" x2={x} y2="80" />
))}
<line className="radio-eq__mid" x1="0" y1="40" x2="220" y2="40" />
<path className="radio-eq__area" d="M0 40C40 40 70 40 110 40S180 40 220 40L220 80L0 80Z" />
<path className="radio-eq__line" d="M0 40C40 40 70 40 110 40S180 40 220 40" />
</svg>
<div className="radio-eq__axis">
<span>60 Hz</span>
<span>1 kHz</span>
<span>10 kHz</span>
</div>
</div>
<div className="radio-eq__opts" role="radiogroup" aria-label="Sound">
{OPTIONS.map((o, i) => (
<label key={o.value} className="radio-eq__opt">
<input className="radio-eq__input" type="radio" name={name} value={o.value} defaultChecked={i === 1} />
{o.label}
</label>
))}
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="radio-eq">
<div class="radio-eq__graph" aria-hidden="true">
<svg viewBox="0 0 220 80">
<line class="radio-eq__grid" x1="0" y1="0" x2="0" y2="80"></line>
<line class="radio-eq__grid" x1="55" y1="0" x2="55" y2="80"></line>
<line class="radio-eq__grid" x1="110" y1="0" x2="110" y2="80"></line>
<line class="radio-eq__grid" x1="165" y1="0" x2="165" y2="80"></line>
<line class="radio-eq__grid" x1="220" y1="0" x2="220" y2="80"></line>
<line class="radio-eq__mid" x1="0" y1="40" x2="220" y2="40"></line>
<path class="radio-eq__area" d="M0 40C40 40 70 40 110 40S180 40 220 40L220 80L0 80Z"></path>
<path class="radio-eq__line" d="M0 40C40 40 70 40 110 40S180 40 220 40"></path>
</svg>
<div class="radio-eq__axis">
<span>60 Hz</span>
<span>1 kHz</span>
<span>10 kHz</span>
</div>
</div>
<div class="radio-eq__opts" role="radiogroup" aria-label="Sound">
<label class="radio-eq__opt"><input class="radio-eq__input" type="radio" name="radio-eq" value="warm"/>Warm</label>
<label class="radio-eq__opt"><input class="radio-eq__input" type="radio" name="radio-eq" checked="" value="flat"/>Flat</label>
<label class="radio-eq__opt"><input class="radio-eq__input" type="radio" name="radio-eq" value="bright"/>Bright</label>
</div>
</div>CSS
.radio-eq { --i: 1; display: grid; justify-items: center; gap: 12px; width: 260px; }
.radio-eq:has(.radio-eq__opt:nth-child(1) input:checked) { --i: 0; }
.radio-eq:has(.radio-eq__opt:nth-child(2) input:checked) { --i: 1; }
.radio-eq:has(.radio-eq__opt:nth-child(3) input:checked) { --i: 2; }
.radio-eq__graph { position: relative; width: 220px; }
.radio-eq__graph svg { display: block; width: 220px; height: 80px; overflow: visible; }
.radio-eq__grid { stroke: light-dark(#ebedf0, #232529); stroke-width: 1; }
.radio-eq__mid { stroke: light-dark(#dbdde1, #2e3035); stroke-dasharray: 2 3; }
.radio-eq__area { fill: color-mix(in oklab, light-dark(#50535b, #c6c8cd) 16%, transparent); transition: d 0.6s cubic-bezier(0.3, 1.2, 0.5, 1); }
.radio-eq__line { fill: none; stroke: light-dark(#373a40, #f1f2f3); stroke-width: 2; stroke-linecap: round; transition: d 0.6s cubic-bezier(0.3, 1.2, 0.5, 1); }
.radio-eq:has(.radio-eq__opt:nth-child(1) input:checked) .radio-eq__line { d: path('M0 22C40 22 70 32 110 42S180 56 220 56'); }
.radio-eq:has(.radio-eq__opt:nth-child(1) input:checked) .radio-eq__area { d: path('M0 22C40 22 70 32 110 42S180 56 220 56L220 80L0 80Z'); }
.radio-eq:has(.radio-eq__opt:nth-child(2) input:checked) .radio-eq__line { d: path('M0 40C40 40 70 40 110 40S180 40 220 40'); }
.radio-eq:has(.radio-eq__opt:nth-child(2) input:checked) .radio-eq__area { d: path('M0 40C40 40 70 40 110 40S180 40 220 40L220 80L0 80Z'); }
.radio-eq:has(.radio-eq__opt:nth-child(3) input:checked) .radio-eq__line { d: path('M0 56C40 56 70 50 110 40S180 22 220 20'); }
.radio-eq:has(.radio-eq__opt:nth-child(3) input:checked) .radio-eq__area { d: path('M0 56C40 56 70 50 110 40S180 22 220 20L220 80L0 80Z'); }
.radio-eq__axis { display: flex; justify-content: space-between; margin-top: 4px; color: light-dark(#5f626a, #a7abb2); font-size: 10px; }
@media (prefers-reduced-motion: reduce) {
.radio-eq__area, .radio-eq__line { transition: none; }
}
.radio-eq__opts { display: flex; gap: 3px; padding: 4px; border-radius: 12px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.radio-eq__opt { position: relative; flex: 1; padding: 7px 10px; border-radius: 8px; color: light-dark(#5f626a, #a7abb2); font-size: 12.5px; font-weight: 500; text-align: center; white-space: nowrap; cursor: pointer; transition: background-color 0.25s, color 0.25s; }
.radio-eq__opt:hover { color: light-dark(#42454c, #e5e6e9); }
.radio-eq__opt:has(input:checked) { background: light-dark(#373a40, #f1f2f3); color: light-dark(#fbfcfd, #121417); }
.radio-eq__opt:has(input:focus-visible) { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
.radio-eq__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }Goal: Create a "EQ" radio.
Visual behavior: A sound picker drawn as an equaliser: choose warm, flat or bright and the curve and its fill bend smoothly into the new shape across the frequencies.
Technique:
- Real native radios (visually hidden: absolute, 1x1, opacity 0) inside <label>s; the scene is aria-hidden and every state is read from the checked input with :has().
- The curve and its filled area are SVG paths whose d is set from CSS with d: path(...) for each option; every shape has the same commands, so the browser transitions d between them.
- A dashed midline and frequency labels mark the flat response. Pure CSS, no script.
Accessibility: A role="radiogroup" with an aria-label around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once; :focus-visible on the input outlines its option. Under prefers-reduced-motion the curve changes shape instantly.
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 "EQ" radio.
Visual behavior: A sound picker drawn as an equaliser: choose warm, flat or bright and the curve and its fill bend smoothly into the new shape across the frequencies.
Technique:
- Real native radios (visually hidden: absolute, 1x1, opacity 0) inside <label>s; the scene is aria-hidden and every state is read from the checked input with :has().
- The curve and its filled area are SVG paths whose d is set from CSS with d: path(...) for each option; every shape has the same commands, so the browser transitions d between them.
- A dashed midline and frequency labels mark the flat response. Pure CSS, no script.
Accessibility: A role="radiogroup" with an aria-label around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once; :focus-visible on the input outlines its option. Under prefers-reduced-motion the curve changes shape instantly.
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 picker drawn as an equaliser: choose warm, flat or bright and the curve and its fill bend smoothly into the new shape across the frequencies.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, morph
- Techniques
- svg, css transition, css variables
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A role="radiogroup" with an aria-label around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once; :focus-visible on the input outlines its option. Under prefers-reduced-motion the curve changes shape instantly.