stroke-dasharray: nonestroke-dasharray: 14 9stroke-dasharray: 0 9
Ringer
React + Tailwind
import { useId } from 'react';
const OPTIONS = [
{ value: 'silent', label: 'Silent', note: 'No sound, no buzz' },
{ value: 'vibrate', label: 'Vibrate', note: 'Buzzes in short bursts' },
{ value: 'ring', label: 'Ring', note: 'Rings out loud' },
];
const at = (n: number) => `.radio-ringer:has(.radio-ringer__opt:nth-child(${n}) input:checked)`;
const css = `
.radio-ringer { display: grid; justify-items: center; gap: 14px; width: 260px; }
.radio-ringer__view { display: block; width: 200px; height: 112px; overflow: visible; }
.radio-ringer__case { fill: light-dark(#dbdde1, #2e3035); }
.radio-ringer__screen { fill: light-dark(#f3f4f7, #1a1c1f); transition: fill 0.4s; }
.radio-ringer__bell { fill: light-dark(#42454c, #e5e6e9); transform-box: view-box; transform-origin: 100px 40px; transition: fill 0.4s; }
.radio-ringer__slash { fill: none; stroke: light-dark(#42454c, #e5e6e9); stroke-width: 2.6; stroke-linecap: round; stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 0.3s; }
.radio-ringer__lines path, .radio-ringer__waves path { fill: none; stroke: light-dark(#50535b, #c6c8cd); stroke-width: 2; stroke-linecap: round; }
.radio-ringer__phone { transform-box: view-box; transform-origin: 100px 56px; }
.radio-ringer__lines, .radio-ringer__waves path { opacity: 0; }
.radio-ringer__waves path { transform-box: view-box; transform-origin: 100px 50px; }
/* Silent: a slash draws across the bell and the screen dims. */
${at(1)} .radio-ringer__slash { stroke-dashoffset: 0; transition-duration: 0.45s; }
${at(1)} .radio-ringer__bell { fill: light-dark(#6d7179, #7d8087); }
${at(1)} .radio-ringer__screen { fill: light-dark(#fbfcfd, #121417); }
/* Vibrate: the phone shudders in short bursts with motion lines either side. */
${at(2)} .radio-ringer__phone { animation: radio-ringer-buzz 1.6s infinite; }
${at(2)} .radio-ringer__lines { animation: radio-ringer-lines 1.6s infinite; }
@keyframes radio-ringer-buzz {
3%, 9%, 15%, 21% { transform: translateX(-2px) rotate(-2deg); }
6%, 12%, 18%, 24% { transform: translateX(2px) rotate(2deg); }
27%, 100% { transform: none; }
}
@keyframes radio-ringer-lines {
2%, 24% { opacity: 1; }
30%, 100% { opacity: 0; }
}
/* Ring: the bell swings and sound waves roll out from the phone. */
${at(3)} .radio-ringer__bell { animation: radio-ringer-swing 1.2s ease-in-out infinite; }
@keyframes radio-ringer-swing {
10% { transform: rotate(16deg); }
20% { transform: rotate(-14deg); }
30% { transform: rotate(10deg); }
40% { transform: rotate(-6deg); }
50%, 100% { transform: none; }
}
${at(3)} .radio-ringer__waves path { animation: radio-ringer-wave 1.2s ease-out infinite; }
${at(3)} .radio-ringer__waves path:nth-child(even) { animation-delay: 0.3s; }
@keyframes radio-ringer-wave {
from { opacity: 0.9; transform: scale(0.8); }
to { opacity: 0; transform: scale(1.25); }
}
.radio-ringer__read { display: grid; color: light-dark(#50535b, #c6c8cd); font-size: 12px; }
.radio-ringer__read > * { grid-area: 1 / 1; text-align: center; opacity: 0; transform: translateY(4px); transition: opacity 0.25s, transform 0.3s; }
${at(1)} .radio-ringer__read > :nth-child(1), ${at(2)} .radio-ringer__read > :nth-child(2), ${at(3)} .radio-ringer__read > :nth-child(3) { opacity: 1; transform: none; }
.radio-ringer__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-ringer__opt { position: relative; flex: 1; padding: 7px 12px; 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-ringer__opt:hover { color: light-dark(#42454c, #e5e6e9); }
.radio-ringer__opt:has(input:checked) { background: light-dark(#373a40, #f1f2f3); color: light-dark(#fbfcfd, #121417); }
.radio-ringer__opt:has(input:focus-visible) { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
.radio-ringer__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
.radio-ringer * { animation: none !important; transition: none !important; }
${at(2)} .radio-ringer__lines, ${at(3)} .radio-ringer__waves path { opacity: 1; }
}
`;
export function RingerRadio() {
const name = useId();
return (
<>
<style>
{css}
</style>
<div className="radio-ringer">
<svg className="radio-ringer__view" viewBox="0 0 200 112" aria-hidden="true">
<g className="radio-ringer__waves">
<path d="M62 40Q54 56 62 72" />
<path d="M52 32Q40 56 52 80" />
<path d="M138 40Q146 56 138 72" />
<path d="M148 32Q160 56 148 80" />
</g>
<g className="radio-ringer__lines">
<path d="M64 46V66" />
<path d="M58 50V62" />
<path d="M136 46V66" />
<path d="M142 50V62" />
</g>
<g className="radio-ringer__phone">
<rect className="radio-ringer__case" x={74} y={8} width={52} height={96} rx={10} />
<rect className="radio-ringer__screen" x={78} y={14} width={44} height={82} rx={6} />
<path className="radio-ringer__bell" d="M100 40C94 40 91 45 91 50V57L88 61H112L109 57V50C109 45 106 40 100 40ZM97 63A3 3 0 0 0 103 63Z" />
<path className="radio-ringer__slash" d="M89 39L111 66" pathLength={1} />
</g>
</svg>
<span className="radio-ringer__read" aria-hidden="true">
{OPTIONS.map((o) => (
<span key={o.value}>{o.note}</span>
))}
</span>
<div className="radio-ringer__opts" role="radiogroup" aria-label="Ringer">
{OPTIONS.map((o, i) => (
<label key={o.value} className="radio-ringer__opt">
<input className="radio-ringer__input" type="radio" name={name} value={o.value} defaultChecked={i === 2} />
{o.label}
</label>
))}
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="radio-ringer">
<svg class="radio-ringer__view" viewBox="0 0 200 112" aria-hidden="true">
<g class="radio-ringer__waves">
<path d="M62 40Q54 56 62 72"></path>
<path d="M52 32Q40 56 52 80"></path>
<path d="M138 40Q146 56 138 72"></path>
<path d="M148 32Q160 56 148 80"></path>
</g>
<g class="radio-ringer__lines">
<path d="M64 46V66"></path>
<path d="M58 50V62"></path>
<path d="M136 46V66"></path>
<path d="M142 50V62"></path>
</g>
<g class="radio-ringer__phone">
<rect class="radio-ringer__case" x="74" y="8" width="52" height="96" rx="10"></rect>
<rect class="radio-ringer__screen" x="78" y="14" width="44" height="82" rx="6"></rect>
<path class="radio-ringer__bell" d="M100 40C94 40 91 45 91 50V57L88 61H112L109 57V50C109 45 106 40 100 40ZM97 63A3 3 0 0 0 103 63Z"></path>
<path class="radio-ringer__slash" d="M89 39L111 66" pathLength="1"></path>
</g>
</svg>
<span class="radio-ringer__read" aria-hidden="true">
<span>No sound, no buzz</span>
<span>Buzzes in short bursts</span>
<span>Rings out loud</span>
</span>
<div class="radio-ringer__opts" role="radiogroup" aria-label="Ringer">
<label class="radio-ringer__opt">
<input class="radio-ringer__input" type="radio" name="radio-ringer" value="silent"/>
Silent
</label>
<label class="radio-ringer__opt">
<input class="radio-ringer__input" type="radio" name="radio-ringer" value="vibrate"/>
Vibrate
</label>
<label class="radio-ringer__opt">
<input class="radio-ringer__input" type="radio" name="radio-ringer" checked="" value="ring"/>
Ring
</label>
</div>
</div>CSS
.radio-ringer { display: grid; justify-items: center; gap: 14px; width: 260px; }
.radio-ringer__view { display: block; width: 200px; height: 112px; overflow: visible; }
.radio-ringer__case { fill: light-dark(#dbdde1, #2e3035); }
.radio-ringer__screen { fill: light-dark(#f3f4f7, #1a1c1f); transition: fill 0.4s; }
.radio-ringer__bell { fill: light-dark(#42454c, #e5e6e9); transform-box: view-box; transform-origin: 100px 40px; transition: fill 0.4s; }
.radio-ringer__slash { fill: none; stroke: light-dark(#42454c, #e5e6e9); stroke-width: 2.6; stroke-linecap: round; stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 0.3s; }
.radio-ringer__lines path, .radio-ringer__waves path { fill: none; stroke: light-dark(#50535b, #c6c8cd); stroke-width: 2; stroke-linecap: round; }
.radio-ringer__phone { transform-box: view-box; transform-origin: 100px 56px; }
.radio-ringer__lines, .radio-ringer__waves path { opacity: 0; }
.radio-ringer__waves path { transform-box: view-box; transform-origin: 100px 50px; }
/* Silent: a slash draws across the bell and the screen dims. */
.radio-ringer:has(.radio-ringer__opt:nth-child(1) input:checked) .radio-ringer__slash { stroke-dashoffset: 0; transition-duration: 0.45s; }
.radio-ringer:has(.radio-ringer__opt:nth-child(1) input:checked) .radio-ringer__bell { fill: light-dark(#6d7179, #7d8087); }
.radio-ringer:has(.radio-ringer__opt:nth-child(1) input:checked) .radio-ringer__screen { fill: light-dark(#fbfcfd, #121417); }
/* Vibrate: the phone shudders in short bursts with motion lines either side. */
.radio-ringer:has(.radio-ringer__opt:nth-child(2) input:checked) .radio-ringer__phone { animation: radio-ringer-buzz 1.6s infinite; }
.radio-ringer:has(.radio-ringer__opt:nth-child(2) input:checked) .radio-ringer__lines { animation: radio-ringer-lines 1.6s infinite; }
@keyframes radio-ringer-buzz {
3%, 9%, 15%, 21% { transform: translateX(-2px) rotate(-2deg); }
6%, 12%, 18%, 24% { transform: translateX(2px) rotate(2deg); }
27%, 100% { transform: none; }
}
@keyframes radio-ringer-lines {
2%, 24% { opacity: 1; }
30%, 100% { opacity: 0; }
}
/* Ring: the bell swings and sound waves roll out from the phone. */
.radio-ringer:has(.radio-ringer__opt:nth-child(3) input:checked) .radio-ringer__bell { animation: radio-ringer-swing 1.2s ease-in-out infinite; }
@keyframes radio-ringer-swing {
10% { transform: rotate(16deg); }
20% { transform: rotate(-14deg); }
30% { transform: rotate(10deg); }
40% { transform: rotate(-6deg); }
50%, 100% { transform: none; }
}
.radio-ringer:has(.radio-ringer__opt:nth-child(3) input:checked) .radio-ringer__waves path { animation: radio-ringer-wave 1.2s ease-out infinite; }
.radio-ringer:has(.radio-ringer__opt:nth-child(3) input:checked) .radio-ringer__waves path:nth-child(even) { animation-delay: 0.3s; }
@keyframes radio-ringer-wave {
from { opacity: 0.9; transform: scale(0.8); }
to { opacity: 0; transform: scale(1.25); }
}
.radio-ringer__read { display: grid; color: light-dark(#50535b, #c6c8cd); font-size: 12px; }
.radio-ringer__read > * { grid-area: 1 / 1; text-align: center; opacity: 0; transform: translateY(4px); transition: opacity 0.25s, transform 0.3s; }
.radio-ringer:has(.radio-ringer__opt:nth-child(1) input:checked) .radio-ringer__read > :nth-child(1), .radio-ringer:has(.radio-ringer__opt:nth-child(2) input:checked) .radio-ringer__read > :nth-child(2), .radio-ringer:has(.radio-ringer__opt:nth-child(3) input:checked) .radio-ringer__read > :nth-child(3) { opacity: 1; transform: none; }
.radio-ringer__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-ringer__opt { position: relative; flex: 1; padding: 7px 12px; 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-ringer__opt:hover { color: light-dark(#42454c, #e5e6e9); }
.radio-ringer__opt:has(input:checked) { background: light-dark(#373a40, #f1f2f3); color: light-dark(#fbfcfd, #121417); }
.radio-ringer__opt:has(input:focus-visible) { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
.radio-ringer__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
.radio-ringer * { animation: none !important; transition: none !important; }
.radio-ringer:has(.radio-ringer__opt:nth-child(2) input:checked) .radio-ringer__lines, .radio-ringer:has(.radio-ringer__opt:nth-child(3) input:checked) .radio-ringer__waves path { opacity: 1; }
}Goal: Create a "Ringer" radio.
Visual behavior: A ringer mode picker: silent draws a slash across the bell, vibrate shudders the phone in short bursts, ring swings the bell with sound waves rolling out.
Technique:
- Native radio inputs sharing one name inside a labelled role="radiogroup", visually hidden but focusable; each option is a <label> wrapping its input, so the arrow keys and clicks both work.
- The scene reacts with :has() on the root, one selector per option (such as :has(.opt:nth-child(2) input:checked)), so no script is needed.
- Silent draws a slash across the bell by its dash offset and dims the screen. Vibrate loops a shudder in short bursts with buzz lines beside the phone. Ring swings the bell and rolls arcs of sound outwards.
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. The drawing is aria-hidden. Under prefers-reduced-motion each mode shows 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 "Ringer" radio.
Visual behavior: A ringer mode picker: silent draws a slash across the bell, vibrate shudders the phone in short bursts, ring swings the bell with sound waves rolling out.
Technique:
- Native radio inputs sharing one name inside a labelled role="radiogroup", visually hidden but focusable; each option is a <label> wrapping its input, so the arrow keys and clicks both work.
- The scene reacts with :has() on the root, one selector per option (such as :has(.opt:nth-child(2) input:checked)), so no script is needed.
- Silent draws a slash across the bell by its dash offset and dims the screen. Vibrate loops a shudder in short bursts with buzz lines beside the phone. Ring swings the bell and rolls arcs of sound outwards.
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. The drawing is aria-hidden. Under prefers-reduced-motion each mode shows 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 ringer mode picker: silent draws a slash across the bell, vibrate shudders the phone in short bursts, ring swings the bell with sound waves rolling out.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, loop, icon
- Techniques
- keyframes, stroke dashoffset, svg
- 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. The drawing is aria-hidden. Under prefers-reduced-motion each mode shows still.