38164725
Big news by FridaySleep on itSay yesWait a week
import { useId, type CSSProperties } from 'react';
// The smaller the duck, the harder the shot.
const LEVELS = [
{ value: 'easy', label: 'Easy', x: 40, w: 62 },
{ value: 'normal', label: 'Normal', x: 102, w: 50 },
{ value: 'hard', label: 'Hard', x: 160, w: 39 },
{ value: 'expert', label: 'Expert', x: 218, w: 29 },
];
const DUCK = 'M4 28C4 21 10 19 18 20C24 21 28 22 30 20C27 17 28 8 36 7C42 7 44 11 43 14L48 15.5L43 17C42 19 40 21 38 22C42 24 43 28 42 31C40 36 34 38 24 38H13C7 38 4 34 4 28ZM5 25L1 17L11 21Z';
const css = `
.radio-fairground { --tin: light-dark(#6d7179, #c6c8cd); --tin-hi: light-dark(#dbdde1, #f1f2f3); --tin-back: light-dark(#50535b, #7d8087); --wall: light-dark(#dbdde1, #1a1c1f); --bulb: light-dark(#f3f4f7, #f1f2f3); position: relative; width: 256px; height: 214px; cursor: crosshair; font-family: Rockwell, 'Roboto Slab', 'Courier New', serif; }
.radio-fairground__sign { position: absolute; left: 50%; top: 0; z-index: 2; translate: -50% 0; padding: 6px 14px; border-radius: 3px; background: light-dark(#f3f4f7, #1a1c1f); color: light-dark(#373a40, #f1f2f3); font-size: 12px; font-weight: 800; letter-spacing: 0.14em; white-space: nowrap; box-shadow: 0 0 0 2px light-dark(#cbced3, #393c41), 0 4px 10px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); }
/* Chase lights: two sets of bulbs round the sign, blinking in turn. */
.radio-fairground__sign::before, .radio-fairground__sign::after { content: ''; position: absolute; inset: 2px; border-radius: 2px; background: radial-gradient(circle, var(--bulb) 1.1px, transparent 1.6px) 0 0 / 8px 8px; mask: linear-gradient(#000 0 0) top / 100% 3px no-repeat, linear-gradient(#000 0 0) bottom / 100% 3px no-repeat; animation: radio-fairground-chase 0.9s steps(1) infinite; }
.radio-fairground__sign::after { background-position: 4px 0; animation-delay: -0.45s; }
@keyframes radio-fairground-chase { 50% { opacity: 0.15; } }
.radio-fairground__awning { position: absolute; left: 0; right: 0; top: 14px; height: 30px; background: repeating-linear-gradient(90deg, light-dark(#373a40, #f1f2f3) 0 16px, light-dark(#cbced3, #393c41) 16px 32px); box-shadow: inset 0 8px 6px -5px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.35)); mask: radial-gradient(circle 8px at 8px 22px, #000 98%, transparent) 0 0 / 16px 100% repeat-x, linear-gradient(#000 0 0) top / 100% 22px no-repeat; filter: drop-shadow(0 4px 3px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4))); }
.radio-fairground__wall { position: absolute; left: 4px; right: 4px; top: 36px; bottom: 26px; background: radial-gradient(circle, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 16%, transparent) 1px, transparent 1.6px) 6px 4px / 20px 20px, repeating-linear-gradient(90deg, transparent 0 31px, color-mix(in oklab, light-dark(#fbfcfd, #121417) 22%, transparent) 31px 32px), var(--wall); box-shadow: inset 0 14px 16px -8px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6)); }
.radio-fairground__rod { position: absolute; left: 0; right: 0; top: 12px; height: 4px; border-radius: 2px; background: light-dark(#6d7179, #7d8087); }
.radio-fairground__rail { position: absolute; left: 4px; right: 4px; bottom: 50px; height: 5px; border-radius: 2px; background: linear-gradient(var(--tin-hi), var(--tin)); box-shadow: 0 2px 3px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); }
.radio-fairground__counter { position: absolute; left: 0; right: 0; bottom: 0; height: 28px; border-radius: 3px; background: linear-gradient(light-dark(#dbdde1, #2e3035), light-dark(#ebedf0, #232529)); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 20%, transparent), 0 -2px 4px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3)); }
.radio-fairground__lane { position: absolute; bottom: 0; top: 70px; width: 58px; translate: -50% 0; cursor: crosshair; }
.radio-fairground__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.radio-fairground__label { position: absolute; left: 0; right: 0; bottom: 8px; color: light-dark(#5f626a, #a7abb2); font-size: 9.5px; font-weight: 800; letter-spacing: 0.12em; text-align: center; text-transform: uppercase; transition: color 0.25s; }
.radio-fairground__lane:hover .radio-fairground__label { color: light-dark(#42454c, #e5e6e9); }
.radio-fairground__lane:has(input:checked) .radio-fairground__label { color: light-dark(#373a40, #f1f2f3); }
.radio-fairground__lane:has(input:focus-visible) .radio-fairground__label { outline: 1.5px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; border-radius: 2px; }
/* A target on its post: tin front, painted back, spun round by the hit. */
.radio-fairground__target { position: absolute; left: 50%; bottom: 56px; width: var(--w); translate: -50% 0; perspective: 300px; animation: radio-fairground-rock 2.8s ease-in-out infinite; transform-origin: 50% 100%; }
.radio-fairground__lane:nth-of-type(2) .radio-fairground__target { animation-delay: -0.7s; }
.radio-fairground__lane:nth-of-type(3) .radio-fairground__target { animation-delay: -1.4s; }
.radio-fairground__lane:nth-of-type(4) .radio-fairground__target { animation-delay: -2.1s; }
@keyframes radio-fairground-rock { 50% { rotate: 3deg; } }
.radio-fairground__post { position: absolute; left: 50%; bottom: -6px; width: 3px; height: 10px; margin-left: -1.5px; background: var(--tin); }
.radio-fairground__spin { position: relative; display: block; aspect-ratio: 48 / 40; transform-style: preserve-3d; transition: transform 0.95s cubic-bezier(0.3, 0, 0.3, 1); }
.radio-fairground__lane:has(input:checked) .radio-fairground__spin { transform: rotateY(900deg); transition: transform 1.3s cubic-bezier(0.12, 0.75, 0.25, 1); }
.radio-fairground__face { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; backface-visibility: hidden; }
.radio-fairground__face--back { transform: rotateY(180deg); }
.radio-fairground__face path { fill: var(--tin); stroke: var(--tin-hi); stroke-width: 1; }
.radio-fairground__face--back path { fill: var(--tin-back); }
.radio-fairground__face circle { fill: none; stroke: var(--wall); stroke-width: 1.6; }
.radio-fairground__face .radio-fairground__eye { fill: var(--wall); stroke: none; }
.radio-fairground__star { fill: var(--tin-hi) !important; stroke: none !important; }
/* The hit: a burst of sparks and a PING. */
.radio-fairground__burst { position: absolute; left: 50%; top: 40%; width: 0; height: 0; pointer-events: none; }
.radio-fairground__burst i { position: absolute; left: -1px; top: -12px; width: 2px; height: 7px; border-radius: 1px; background: light-dark(#373a40, #f1f2f3); opacity: 0; transform-origin: 1px 12px; rotate: calc(var(--k) * 60deg); }
.radio-fairground__lane:has(input:checked) .radio-fairground__burst i { animation: radio-fairground-spark 0.45s ease-out; }
@keyframes radio-fairground-spark { 0% { opacity: 1; transform: translateY(6px) scaleY(0.4); } 100% { opacity: 0; transform: translateY(-8px) scaleY(1); } }
.radio-fairground__ping { position: absolute; left: 50%; top: -20px; color: light-dark(#373a40, #f1f2f3); font-size: 10px; font-weight: 800; letter-spacing: 0.08em; opacity: 0; translate: -50% 0; pointer-events: none; }
.radio-fairground__lane:has(input:checked) .radio-fairground__ping { animation: radio-fairground-ping 0.8s cubic-bezier(0.2, 0.9, 0.3, 1); }
@keyframes radio-fairground-ping { 0% { opacity: 1; transform: scale(0.5); } 60% { opacity: 1; transform: translateY(-6px) scale(1.1); } 100% { opacity: 0; transform: translateY(-10px); } }
@media (prefers-reduced-motion: reduce) {
.radio-fairground *, .radio-fairground *::before, .radio-fairground *::after { animation: none !important; transition: none !important; }
}
`;
function Duck({ back }: { back?: boolean }) {
return (
<svg className={`radio-fairground__face${back ? ' radio-fairground__face--back' : ''}`} viewBox="0 0 48 40" aria-hidden="true">
<path d={DUCK} />
{back ? (
<path className="radio-fairground__star" d="M24 21.5l2.1 4.3 4.7.6-3.4 3.3.8 4.6-4.2-2.2-4.2 2.2.8-4.6-3.4-3.3 4.7-.6z" />
) : (
<>
<circle cx={19} cy={29} r={5} />
<circle cx={19} cy={29} r={1.4} />
<circle className="radio-fairground__eye" cx={37} cy={12} r={1.3} />
</>
)}
</svg>
);
}
export function Preview() {
const name = useId();
return (
<>
<style href="kit-radios-fairground" precedence="kit">
{css}
</style>
<div className="radio-fairground" role="radiogroup" aria-label="Difficulty">
<span className="radio-fairground__wall" aria-hidden="true" />
<span className="radio-fairground__awning" aria-hidden="true" />
<span className="radio-fairground__rod" aria-hidden="true" />
<span className="radio-fairground__sign" aria-hidden="true">
PICK A LEVEL
</span>
<span className="radio-fairground__rail" aria-hidden="true" />
<span className="radio-fairground__counter" aria-hidden="true" />
{LEVELS.map((l, i) => (
<label key={l.value} className="radio-fairground__lane" style={{ left: l.x, '--w': `${l.w}px` } as CSSProperties}>
<input className="radio-fairground__input" type="radio" name={name} value={l.value} defaultChecked={i === 1} />
<span className="radio-fairground__target" aria-hidden="true">
<span className="radio-fairground__post" />
<span className="radio-fairground__spin">
<Duck />
<Duck back />
</span>
<span className="radio-fairground__burst">
{[0, 1, 2, 3, 4, 5].map((k) => (
<i key={k} style={{ '--k': k } as CSSProperties} />
))}
</span>
<span className="radio-fairground__ping">PING</span>
</span>
<span className="radio-fairground__label">{l.label}</span>
</label>
))}
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<div class="radio-fairground" role="radiogroup" aria-label="Difficulty">
<span class="radio-fairground__wall" aria-hidden="true"></span>
<span class="radio-fairground__awning" aria-hidden="true"></span>
<span class="radio-fairground__rod" aria-hidden="true"></span>
<span class="radio-fairground__sign" aria-hidden="true">PICK A LEVEL</span>
<span class="radio-fairground__rail" aria-hidden="true"></span>
<span class="radio-fairground__counter" aria-hidden="true"></span>
<label class="radio-fairground__lane" style="left:40px;--w:62px">
<input class="radio-fairground__input" type="radio" name="radio-fairground" value="easy"/>
<span class="radio-fairground__target" aria-hidden="true">
<span class="radio-fairground__post"></span>
<span class="radio-fairground__spin">
<svg class="radio-fairground__face" viewBox="0 0 48 40" aria-hidden="true">
<path d="M4 28C4 21 10 19 18 20C24 21 28 22 30 20C27 17 28 8 36 7C42 7 44 11 43 14L48 15.5L43 17C42 19 40 21 38 22C42 24 43 28 42 31C40 36 34 38 24 38H13C7 38 4 34 4 28ZM5 25L1 17L11 21Z"></path>
<circle cx="19" cy="29" r="5"></circle>
<circle cx="19" cy="29" r="1.4"></circle>
<circle class="radio-fairground__eye" cx="37" cy="12" r="1.3"></circle>
</svg>
<svg class="radio-fairground__face radio-fairground__face--back" viewBox="0 0 48 40" aria-hidden="true">
<path d="M4 28C4 21 10 19 18 20C24 21 28 22 30 20C27 17 28 8 36 7C42 7 44 11 43 14L48 15.5L43 17C42 19 40 21 38 22C42 24 43 28 42 31C40 36 34 38 24 38H13C7 38 4 34 4 28ZM5 25L1 17L11 21Z"></path>
<path class="radio-fairground__star" d="M24 21.5l2.1 4.3 4.7.6-3.4 3.3.8 4.6-4.2-2.2-4.2 2.2.8-4.6-3.4-3.3 4.7-.6z"></path>
</svg>
</span>
<span class="radio-fairground__burst">
<i style="--k:0"></i>
<i style="--k:1"></i>
<i style="--k:2"></i>
<i style="--k:3"></i>
<i style="--k:4"></i>
<i style="--k:5"></i>
</span>
<span class="radio-fairground__ping">PING</span>
</span>
<span class="radio-fairground__label">Easy</span>
</label>
<label class="radio-fairground__lane" style="left:102px;--w:50px">
<input class="radio-fairground__input" type="radio" name="radio-fairground" checked="" value="normal"/>
<span class="radio-fairground__target" aria-hidden="true">
<span class="radio-fairground__post"></span>
<span class="radio-fairground__spin">
<svg class="radio-fairground__face" viewBox="0 0 48 40" aria-hidden="true">
<path d="M4 28C4 21 10 19 18 20C24 21 28 22 30 20C27 17 28 8 36 7C42 7 44 11 43 14L48 15.5L43 17C42 19 40 21 38 22C42 24 43 28 42 31C40 36 34 38 24 38H13C7 38 4 34 4 28ZM5 25L1 17L11 21Z"></path>
<circle cx="19" cy="29" r="5"></circle>
<circle cx="19" cy="29" r="1.4"></circle>
<circle class="radio-fairground__eye" cx="37" cy="12" r="1.3"></circle>
</svg>
<svg class="radio-fairground__face radio-fairground__face--back" viewBox="0 0 48 40" aria-hidden="true">
<path d="M4 28C4 21 10 19 18 20C24 21 28 22 30 20C27 17 28 8 36 7C42 7 44 11 43 14L48 15.5L43 17C42 19 40 21 38 22C42 24 43 28 42 31C40 36 34 38 24 38H13C7 38 4 34 4 28ZM5 25L1 17L11 21Z"></path>
<path class="radio-fairground__star" d="M24 21.5l2.1 4.3 4.7.6-3.4 3.3.8 4.6-4.2-2.2-4.2 2.2.8-4.6-3.4-3.3 4.7-.6z"></path>
</svg>
</span>
<span class="radio-fairground__burst">
<i style="--k:0"></i>
<i style="--k:1"></i>
<i style="--k:2"></i>
<i style="--k:3"></i>
<i style="--k:4"></i>
<i style="--k:5"></i>
</span>
<span class="radio-fairground__ping">PING</span>
</span>
<span class="radio-fairground__label">Normal</span>
</label>
<label class="radio-fairground__lane" style="left:160px;--w:39px">
<input class="radio-fairground__input" type="radio" name="radio-fairground" value="hard"/>
<span class="radio-fairground__target" aria-hidden="true">
<span class="radio-fairground__post"></span>
<span class="radio-fairground__spin">
<svg class="radio-fairground__face" viewBox="0 0 48 40" aria-hidden="true">
<path d="M4 28C4 21 10 19 18 20C24 21 28 22 30 20C27 17 28 8 36 7C42 7 44 11 43 14L48 15.5L43 17C42 19 40 21 38 22C42 24 43 28 42 31C40 36 34 38 24 38H13C7 38 4 34 4 28ZM5 25L1 17L11 21Z"></path>
<circle cx="19" cy="29" r="5"></circle>
<circle cx="19" cy="29" r="1.4"></circle>
<circle class="radio-fairground__eye" cx="37" cy="12" r="1.3"></circle>
</svg>
<svg class="radio-fairground__face radio-fairground__face--back" viewBox="0 0 48 40" aria-hidden="true">
<path d="M4 28C4 21 10 19 18 20C24 21 28 22 30 20C27 17 28 8 36 7C42 7 44 11 43 14L48 15.5L43 17C42 19 40 21 38 22C42 24 43 28 42 31C40 36 34 38 24 38H13C7 38 4 34 4 28ZM5 25L1 17L11 21Z"></path>
<path class="radio-fairground__star" d="M24 21.5l2.1 4.3 4.7.6-3.4 3.3.8 4.6-4.2-2.2-4.2 2.2.8-4.6-3.4-3.3 4.7-.6z"></path>
</svg>
</span>
<span class="radio-fairground__burst">
<i style="--k:0"></i>
<i style="--k:1"></i>
<i style="--k:2"></i>
<i style="--k:3"></i>
<i style="--k:4"></i>
<i style="--k:5"></i>
</span>
<span class="radio-fairground__ping">PING</span>
</span>
<span class="radio-fairground__label">Hard</span>
</label>
<label class="radio-fairground__lane" style="left:218px;--w:29px">
<input class="radio-fairground__input" type="radio" name="radio-fairground" value="expert"/>
<span class="radio-fairground__target" aria-hidden="true">
<span class="radio-fairground__post"></span>
<span class="radio-fairground__spin">
<svg class="radio-fairground__face" viewBox="0 0 48 40" aria-hidden="true">
<path d="M4 28C4 21 10 19 18 20C24 21 28 22 30 20C27 17 28 8 36 7C42 7 44 11 43 14L48 15.5L43 17C42 19 40 21 38 22C42 24 43 28 42 31C40 36 34 38 24 38H13C7 38 4 34 4 28ZM5 25L1 17L11 21Z"></path>
<circle cx="19" cy="29" r="5"></circle>
<circle cx="19" cy="29" r="1.4"></circle>
<circle class="radio-fairground__eye" cx="37" cy="12" r="1.3"></circle>
</svg>
<svg class="radio-fairground__face radio-fairground__face--back" viewBox="0 0 48 40" aria-hidden="true">
<path d="M4 28C4 21 10 19 18 20C24 21 28 22 30 20C27 17 28 8 36 7C42 7 44 11 43 14L48 15.5L43 17C42 19 40 21 38 22C42 24 43 28 42 31C40 36 34 38 24 38H13C7 38 4 34 4 28ZM5 25L1 17L11 21Z"></path>
<path class="radio-fairground__star" d="M24 21.5l2.1 4.3 4.7.6-3.4 3.3.8 4.6-4.2-2.2-4.2 2.2.8-4.6-3.4-3.3 4.7-.6z"></path>
</svg>
</span>
<span class="radio-fairground__burst">
<i style="--k:0"></i>
<i style="--k:1"></i>
<i style="--k:2"></i>
<i style="--k:3"></i>
<i style="--k:4"></i>
<i style="--k:5"></i>
</span>
<span class="radio-fairground__ping">PING</span>
</span>
<span class="radio-fairground__label">Expert</span>
</label>
</div>.radio-fairground { --tin: light-dark(#6d7179, #c6c8cd); --tin-hi: light-dark(#dbdde1, #f1f2f3); --tin-back: light-dark(#50535b, #7d8087); --wall: light-dark(#dbdde1, #1a1c1f); --bulb: light-dark(#f3f4f7, #f1f2f3); position: relative; width: 256px; height: 214px; cursor: crosshair; font-family: Rockwell, 'Roboto Slab', 'Courier New', serif; }
.radio-fairground__sign { position: absolute; left: 50%; top: 0; z-index: 2; translate: -50% 0; padding: 6px 14px; border-radius: 3px; background: light-dark(#f3f4f7, #1a1c1f); color: light-dark(#373a40, #f1f2f3); font-size: 12px; font-weight: 800; letter-spacing: 0.14em; white-space: nowrap; box-shadow: 0 0 0 2px light-dark(#cbced3, #393c41), 0 4px 10px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); }
/* Chase lights: two sets of bulbs round the sign, blinking in turn. */
.radio-fairground__sign::before, .radio-fairground__sign::after { content: ''; position: absolute; inset: 2px; border-radius: 2px; background: radial-gradient(circle, var(--bulb) 1.1px, transparent 1.6px) 0 0 / 8px 8px; mask: linear-gradient(#000 0 0) top / 100% 3px no-repeat, linear-gradient(#000 0 0) bottom / 100% 3px no-repeat; animation: radio-fairground-chase 0.9s steps(1) infinite; }
.radio-fairground__sign::after { background-position: 4px 0; animation-delay: -0.45s; }
@keyframes radio-fairground-chase { 50% { opacity: 0.15; } }
.radio-fairground__awning { position: absolute; left: 0; right: 0; top: 14px; height: 30px; background: repeating-linear-gradient(90deg, light-dark(#373a40, #f1f2f3) 0 16px, light-dark(#cbced3, #393c41) 16px 32px); box-shadow: inset 0 8px 6px -5px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.35)); mask: radial-gradient(circle 8px at 8px 22px, #000 98%, transparent) 0 0 / 16px 100% repeat-x, linear-gradient(#000 0 0) top / 100% 22px no-repeat; filter: drop-shadow(0 4px 3px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4))); }
.radio-fairground__wall { position: absolute; left: 4px; right: 4px; top: 36px; bottom: 26px; background: radial-gradient(circle, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 16%, transparent) 1px, transparent 1.6px) 6px 4px / 20px 20px, repeating-linear-gradient(90deg, transparent 0 31px, color-mix(in oklab, light-dark(#fbfcfd, #121417) 22%, transparent) 31px 32px), var(--wall); box-shadow: inset 0 14px 16px -8px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6)); }
.radio-fairground__rod { position: absolute; left: 0; right: 0; top: 12px; height: 4px; border-radius: 2px; background: light-dark(#6d7179, #7d8087); }
.radio-fairground__rail { position: absolute; left: 4px; right: 4px; bottom: 50px; height: 5px; border-radius: 2px; background: linear-gradient(var(--tin-hi), var(--tin)); box-shadow: 0 2px 3px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); }
.radio-fairground__counter { position: absolute; left: 0; right: 0; bottom: 0; height: 28px; border-radius: 3px; background: linear-gradient(light-dark(#dbdde1, #2e3035), light-dark(#ebedf0, #232529)); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 20%, transparent), 0 -2px 4px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3)); }
.radio-fairground__lane { position: absolute; bottom: 0; top: 70px; width: 58px; translate: -50% 0; cursor: crosshair; }
.radio-fairground__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.radio-fairground__label { position: absolute; left: 0; right: 0; bottom: 8px; color: light-dark(#5f626a, #a7abb2); font-size: 9.5px; font-weight: 800; letter-spacing: 0.12em; text-align: center; text-transform: uppercase; transition: color 0.25s; }
.radio-fairground__lane:hover .radio-fairground__label { color: light-dark(#42454c, #e5e6e9); }
.radio-fairground__lane:has(input:checked) .radio-fairground__label { color: light-dark(#373a40, #f1f2f3); }
.radio-fairground__lane:has(input:focus-visible) .radio-fairground__label { outline: 1.5px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; border-radius: 2px; }
/* A target on its post: tin front, painted back, spun round by the hit. */
.radio-fairground__target { position: absolute; left: 50%; bottom: 56px; width: var(--w); translate: -50% 0; perspective: 300px; animation: radio-fairground-rock 2.8s ease-in-out infinite; transform-origin: 50% 100%; }
.radio-fairground__lane:nth-of-type(2) .radio-fairground__target { animation-delay: -0.7s; }
.radio-fairground__lane:nth-of-type(3) .radio-fairground__target { animation-delay: -1.4s; }
.radio-fairground__lane:nth-of-type(4) .radio-fairground__target { animation-delay: -2.1s; }
@keyframes radio-fairground-rock { 50% { rotate: 3deg; } }
.radio-fairground__post { position: absolute; left: 50%; bottom: -6px; width: 3px; height: 10px; margin-left: -1.5px; background: var(--tin); }
.radio-fairground__spin { position: relative; display: block; aspect-ratio: 48 / 40; transform-style: preserve-3d; transition: transform 0.95s cubic-bezier(0.3, 0, 0.3, 1); }
.radio-fairground__lane:has(input:checked) .radio-fairground__spin { transform: rotateY(900deg); transition: transform 1.3s cubic-bezier(0.12, 0.75, 0.25, 1); }
.radio-fairground__face { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; backface-visibility: hidden; }
.radio-fairground__face--back { transform: rotateY(180deg); }
.radio-fairground__face path { fill: var(--tin); stroke: var(--tin-hi); stroke-width: 1; }
.radio-fairground__face--back path { fill: var(--tin-back); }
.radio-fairground__face circle { fill: none; stroke: var(--wall); stroke-width: 1.6; }
.radio-fairground__face .radio-fairground__eye { fill: var(--wall); stroke: none; }
.radio-fairground__star { fill: var(--tin-hi) !important; stroke: none !important; }
/* The hit: a burst of sparks and a PING. */
.radio-fairground__burst { position: absolute; left: 50%; top: 40%; width: 0; height: 0; pointer-events: none; }
.radio-fairground__burst i { position: absolute; left: -1px; top: -12px; width: 2px; height: 7px; border-radius: 1px; background: light-dark(#373a40, #f1f2f3); opacity: 0; transform-origin: 1px 12px; rotate: calc(var(--k) * 60deg); }
.radio-fairground__lane:has(input:checked) .radio-fairground__burst i { animation: radio-fairground-spark 0.45s ease-out; }
@keyframes radio-fairground-spark { 0% { opacity: 1; transform: translateY(6px) scaleY(0.4); } 100% { opacity: 0; transform: translateY(-8px) scaleY(1); } }
.radio-fairground__ping { position: absolute; left: 50%; top: -20px; color: light-dark(#373a40, #f1f2f3); font-size: 10px; font-weight: 800; letter-spacing: 0.08em; opacity: 0; translate: -50% 0; pointer-events: none; }
.radio-fairground__lane:has(input:checked) .radio-fairground__ping { animation: radio-fairground-ping 0.8s cubic-bezier(0.2, 0.9, 0.3, 1); }
@keyframes radio-fairground-ping { 0% { opacity: 1; transform: scale(0.5); } 60% { opacity: 1; transform: translateY(-6px) scale(1.1); } 100% { opacity: 0; transform: translateY(-10px); } }
@media (prefers-reduced-motion: reduce) {
.radio-fairground *, .radio-fairground *::before, .radio-fairground *::after { animation: none !important; transition: none !important; }
}Goal: Create a "Fairground" radio.
Visual behavior: A shooting gallery difficulty picker where the smaller the duck, the harder the shot. Hit a tin target and it spins round on its post with sparks and a PING to show its star, while the last one spins back.
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.
- Each duck has a tin front and a painted back with a star, set back to back with backface-visibility hidden. Picking one transitions it to rotateY(900deg), two and a half turns that end on the back, fast and then slowing; the last one spins back to 0.
- The sparks and the PING are keyframes on each target that start when its input becomes checked. The awning is a striped gradient with a scalloped edge cut by a mask, and the sign's chase lights are two dotted layers blinking in turn.
Accessibility: A role="radiogroup" labelled "Difficulty" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. The level names on the counter are the visible labels and focus outlines them; the ducks are aria-hidden. Under prefers-reduced-motion the targets turn at once and the lights stop.
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 "Fairground" radio.
Visual behavior: A shooting gallery difficulty picker where the smaller the duck, the harder the shot. Hit a tin target and it spins round on its post with sparks and a PING to show its star, while the last one spins back.
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.
- Each duck has a tin front and a painted back with a star, set back to back with backface-visibility hidden. Picking one transitions it to rotateY(900deg), two and a half turns that end on the back, fast and then slowing; the last one spins back to 0.
- The sparks and the PING are keyframes on each target that start when its input becomes checked. The awning is a striped gradient with a scalloped edge cut by a mask, and the sign's chase lights are two dotted layers blinking in turn.
Accessibility: A role="radiogroup" labelled "Difficulty" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. The level names on the counter are the visible labels and focus outlines them; the ducks are aria-hidden. Under prefers-reduced-motion the targets turn at once and the lights stop.
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
A shooting gallery difficulty picker where the smaller the duck, the harder the shot. Hit a tin target and it spins round on its post with sparks and a PING to show its star, while the last one spins back.
A role="radiogroup" labelled "Difficulty" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. The level names on the counter are the visible labels and focus outlines them; the ducks are aria-hidden. Under prefers-reduced-motion the targets turn at once and the lights stop.