eE4 · 329.63 HzBB3 · 246.94 HzGG3 · 196.00 HzDD3 · 146.83 HzAA2 · 110.00 HzEE2 · 82.41 Hz
Stickers
React + Tailwind
import { useId, type CSSProperties } from 'react';
// Six die-cut stickers on a backing sheet. Each is drawn in a 44×44 box; the
// first path is the vinyl (with its white cut margin as a stroke), the rest
// are the printed details.
const STICKERS = [
{ value: 'bolt', label: 'Bolt', d: ['M25 4L9 25H21L17 40L35 17H23Z'] },
{ value: 'heart', label: 'Heart', d: ['M22 38C9 29 5 22 5 15.5C5 10 9 6 14 6C17.5 6 20.5 8 22 11C23.5 8 26.5 6 30 6C35 6 39 10 39 15.5C39 22 35 29 22 38Z', 'M12 13.5C12.6 11.5 14 10.5 15.8 10.3'] },
{ value: 'smile', label: 'Smile', d: ['M22 5A17 17 0 1 1 21.9 5Z', 'M15.5 17V19.5M28.5 17V19.5M14 26C17.5 31 26.5 31 30 26'] },
{ value: 'star', label: 'Star', d: ['M22 4.5L27 15.6L39 16.9L30 25L32.6 37L22 30.9L11.4 37L14 25L5 16.9L17 15.6Z'] },
{ value: 'ghost', label: 'Ghost', d: ['M8 38V19C8 11 14 5.5 22 5.5C30 5.5 36 11 36 19V38L31.3 34L26.6 38L22 34L17.4 38L12.7 34Z', 'M17 17.5V20.5M27 17.5V20.5'] },
{ value: 'flower', label: 'Flower', d: ['M22 7.5A6.5 6.5 0 0 1 28.1 16.3A6.5 6.5 0 0 1 32.3 27.5A6.5 6.5 0 0 1 22 33.8A6.5 6.5 0 0 1 11.7 27.5A6.5 6.5 0 0 1 15.9 16.3A6.5 6.5 0 0 1 22 7.5Z', 'M22 18.5A3.2 3.2 0 1 1 21.9 18.5Z'] },
];
// Sheet slots (centres) and the spot on the laptop lid they land on.
const SLOT = STICKERS.map((_, i) => ({ x: 158 + (i % 2) * 48, y: 42 + Math.floor(i / 2) * 47 }));
const LID = { x: 58, y: 84 };
const TILT = [-9, 7, -5, 10, -7, 6];
const at = (n: number) => `.radio-stickers:has(.radio-stickers__slot:nth-child(${n + 1}) input:checked)`;
const css = `
.radio-stickers { --vinyl: light-dark(#373a40, #232529); --cut: light-dark(#fbfcfd, #f1f2f3); --sheet: light-dark(#ebedf0, #c6c8cd); --lid: light-dark(#dbdde1, #2e3035); position: relative; width: 254px; height: 196px; perspective: 600px; }
.radio-stickers__laptop { position: absolute; left: 2px; top: 38px; width: 112px; height: 80px; border-radius: 9px; background: radial-gradient(130% 90% at 30% 10%, color-mix(in oklab, var(--lid) 80%, light-dark(#373a40, #f1f2f3)), var(--lid) 60%); box-shadow: inset 0 0 0 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 12%, transparent), 0 12px 18px -8px light-dark(rgba(0, 0, 0, 0.17), rgba(0, 0, 0, 0.55)); }
.radio-stickers__laptop::after { content: ''; position: absolute; left: 50%; bottom: -6px; width: 124px; height: 6px; translate: -50% 0; border-radius: 0 0 6px 6px; background: color-mix(in oklab, var(--lid) 80%, light-dark(#fbfcfd, #121417)); }
.radio-stickers__read { position: absolute; left: 2px; top: 136px; width: 112px; display: grid; color: light-dark(#373a40, #f1f2f3); font: 900 13px/1 ui-rounded, 'SF Pro Rounded', 'Arial Rounded MT Bold', system-ui, sans-serif; letter-spacing: 0.04em; text-align: center; text-transform: uppercase; }
.radio-stickers__read > span { grid-area: 1 / 1; opacity: 0; transform: translateY(4px); transition: opacity 0.2s, transform 0.3s; }
.radio-stickers__read small { display: block; margin-top: 4px; color: light-dark(#6d7179, #7d8087); font-size: 9px; font-weight: 600; letter-spacing: 0.06em; text-transform: none; }
${STICKERS.map((_, i) => `${at(i)} .radio-stickers__read > :nth-child(${i + 1})`).join(', ')} { opacity: 1; transform: none; transition-delay: 0.55s; }
.radio-stickers__sheet { position: absolute; left: 128px; top: 12px; width: 108px; height: 150px; border-radius: 6px; background: linear-gradient(155deg, color-mix(in oklab, var(--sheet) 80%, light-dark(#fbfcfd, #121417)), var(--sheet) 40%); box-shadow: 0 1px 2px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.25)), 0 10px 18px -8px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); transform: rotate(1.2deg); }
.radio-stickers__slot { position: absolute; width: 44px; height: 44px; cursor: pointer; }
.radio-stickers__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.radio-stickers__ghost { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; fill: none; stroke: color-mix(in oklab, var(--sheet) 60%, light-dark(#fbfcfd, #121417)); stroke-width: 1; stroke-dasharray: 2 2; }
/* The sticker: peeled by a corner, flown over, slapped down on the lid. */
.radio-stickers__sticker { position: absolute; inset: 0; transform-origin: 70% 70%; transition: transform 0.55s cubic-bezier(0.45, 0, 0.2, 1); filter: drop-shadow(0 1px 1px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); }
.radio-stickers__sticker svg { display: block; width: 100%; height: 100%; overflow: visible; }
.radio-stickers__sticker path:first-child { fill: var(--vinyl); stroke: var(--cut); stroke-width: 4.5; stroke-linejoin: round; paint-order: stroke; }
.radio-stickers__sticker path + path { fill: none; stroke: var(--cut); stroke-width: 2.6; stroke-linecap: round; }
.radio-stickers__slot:hover .radio-stickers__sticker { transform: rotate(-4deg) scale(1.06); }
.radio-stickers__slot:has(input:focus-visible) .radio-stickers__ghost { stroke: light-dark(#373a40, #f1f2f3); stroke-dasharray: none; stroke-width: 2; }
.radio-stickers__slot:has(input:checked) { z-index: 2; }
.radio-stickers__slot:has(input:checked) .radio-stickers__sticker { transform: translate(var(--tx), var(--ty)) rotate(var(--rot)) scale(1.32); transition: none; animation: radio-stickers-peel 0.85s cubic-bezier(0.4, 0, 0.2, 1); }
@keyframes radio-stickers-peel {
0% { transform: none; }
22% { transform: translate(-3px, -5px) rotate3d(1, -1, 0, 38deg) scale(1.06); filter: drop-shadow(0 6px 5px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3))); }
68% { transform: translate(calc(var(--tx) + 4px), calc(var(--ty) - 16px)) rotate(var(--rot)) scale(1.48); filter: drop-shadow(0 16px 12px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3))); }
86% { transform: translate(var(--tx), var(--ty)) rotate(var(--rot)) scale(1.24); }
100% { transform: translate(var(--tx), var(--ty)) rotate(var(--rot)) scale(1.32); }
}
.radio-stickers__slot:has(input:checked) .radio-stickers__shine { animation: radio-stickers-shine 0.6s ease-out 0.8s; }
.radio-stickers__shine { position: absolute; inset: 0; border-radius: 50%; background: linear-gradient(115deg, transparent 35%, color-mix(in oklab, var(--cut) 55%, transparent) 50%, transparent 65%) -60px 0 / 200% 100% no-repeat; opacity: 0; pointer-events: none; mix-blend-mode: overlay; }
@keyframes radio-stickers-shine { 0% { opacity: 1; background-position: -60px 0; } 100% { opacity: 1; background-position: 60px 0; } }
@media (prefers-reduced-motion: reduce) {
.radio-stickers * { animation: none !important; transition: none !important; }
}
`;
export function Preview() {
const name = useId();
return (
<>
<style href="kit-radios-stickers" precedence="kit">
{css}
</style>
<div className="radio-stickers">
<span className="radio-stickers__laptop" aria-hidden="true" />
<span className="radio-stickers__sheet" aria-hidden="true" />
<p className="radio-stickers__read" aria-hidden="true">
{STICKERS.map((s) => (
<span key={s.value}>
{s.label}
<small>on your laptop</small>
</span>
))}
</p>
<div role="radiogroup" aria-label="Badge">
{STICKERS.map((s, i) => (
<label
key={s.value}
className="radio-stickers__slot"
style={{ left: SLOT[i].x - 22, top: SLOT[i].y - 22, '--tx': `${LID.x - SLOT[i].x}px`, '--ty': `${LID.y - SLOT[i].y}px`, '--rot': `${TILT[i]}deg` } as CSSProperties}
>
<input className="radio-stickers__input" type="radio" name={name} value={s.value} aria-label={s.label} defaultChecked={i === 4} />
<svg className="radio-stickers__ghost" viewBox="0 0 44 44" aria-hidden="true">
<path d={s.d[0]} />
</svg>
<span className="radio-stickers__sticker" aria-hidden="true">
<svg viewBox="0 0 44 44">
{s.d.map((p, k) => (
<path key={k} d={p} />
))}
</svg>
<span className="radio-stickers__shine" />
</span>
</label>
))}
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="radio-stickers">
<span class="radio-stickers__laptop" aria-hidden="true"></span>
<span class="radio-stickers__sheet" aria-hidden="true"></span>
<p class="radio-stickers__read" aria-hidden="true">
<span>
Bolt
<small>on your laptop</small>
</span>
<span>
Heart
<small>on your laptop</small>
</span>
<span>
Smile
<small>on your laptop</small>
</span>
<span>
Star
<small>on your laptop</small>
</span>
<span>
Ghost
<small>on your laptop</small>
</span>
<span>
Flower
<small>on your laptop</small>
</span>
</p>
<div role="radiogroup" aria-label="Badge">
<label class="radio-stickers__slot" style="left:136px;top:20px;--tx:-100px;--ty:42px;--rot:-9deg">
<input class="radio-stickers__input" type="radio" aria-label="Bolt" name="radio-stickers" value="bolt"/>
<svg class="radio-stickers__ghost" viewBox="0 0 44 44" aria-hidden="true">
<path d="M25 4L9 25H21L17 40L35 17H23Z"></path>
</svg>
<span class="radio-stickers__sticker" aria-hidden="true">
<svg viewBox="0 0 44 44">
<path d="M25 4L9 25H21L17 40L35 17H23Z"></path>
</svg>
<span class="radio-stickers__shine"></span>
</span>
</label>
<label class="radio-stickers__slot" style="left:184px;top:20px;--tx:-148px;--ty:42px;--rot:7deg">
<input class="radio-stickers__input" type="radio" aria-label="Heart" name="radio-stickers" value="heart"/>
<svg class="radio-stickers__ghost" viewBox="0 0 44 44" aria-hidden="true">
<path d="M22 38C9 29 5 22 5 15.5C5 10 9 6 14 6C17.5 6 20.5 8 22 11C23.5 8 26.5 6 30 6C35 6 39 10 39 15.5C39 22 35 29 22 38Z"></path>
</svg>
<span class="radio-stickers__sticker" aria-hidden="true">
<svg viewBox="0 0 44 44">
<path d="M22 38C9 29 5 22 5 15.5C5 10 9 6 14 6C17.5 6 20.5 8 22 11C23.5 8 26.5 6 30 6C35 6 39 10 39 15.5C39 22 35 29 22 38Z"></path>
<path d="M12 13.5C12.6 11.5 14 10.5 15.8 10.3"></path>
</svg>
<span class="radio-stickers__shine"></span>
</span>
</label>
<label class="radio-stickers__slot" style="left:136px;top:67px;--tx:-100px;--ty:-5px;--rot:-5deg">
<input class="radio-stickers__input" type="radio" aria-label="Smile" name="radio-stickers" value="smile"/>
<svg class="radio-stickers__ghost" viewBox="0 0 44 44" aria-hidden="true">
<path d="M22 5A17 17 0 1 1 21.9 5Z"></path>
</svg>
<span class="radio-stickers__sticker" aria-hidden="true">
<svg viewBox="0 0 44 44">
<path d="M22 5A17 17 0 1 1 21.9 5Z"></path>
<path d="M15.5 17V19.5M28.5 17V19.5M14 26C17.5 31 26.5 31 30 26"></path>
</svg>
<span class="radio-stickers__shine"></span>
</span>
</label>
<label class="radio-stickers__slot" style="left:184px;top:67px;--tx:-148px;--ty:-5px;--rot:10deg">
<input class="radio-stickers__input" type="radio" aria-label="Star" name="radio-stickers" value="star"/>
<svg class="radio-stickers__ghost" viewBox="0 0 44 44" aria-hidden="true">
<path d="M22 4.5L27 15.6L39 16.9L30 25L32.6 37L22 30.9L11.4 37L14 25L5 16.9L17 15.6Z"></path>
</svg>
<span class="radio-stickers__sticker" aria-hidden="true">
<svg viewBox="0 0 44 44">
<path d="M22 4.5L27 15.6L39 16.9L30 25L32.6 37L22 30.9L11.4 37L14 25L5 16.9L17 15.6Z"></path>
</svg>
<span class="radio-stickers__shine"></span>
</span>
</label>
<label class="radio-stickers__slot" style="left:136px;top:114px;--tx:-100px;--ty:-52px;--rot:-7deg">
<input class="radio-stickers__input" type="radio" aria-label="Ghost" name="radio-stickers" checked="" value="ghost"/>
<svg class="radio-stickers__ghost" viewBox="0 0 44 44" aria-hidden="true">
<path d="M8 38V19C8 11 14 5.5 22 5.5C30 5.5 36 11 36 19V38L31.3 34L26.6 38L22 34L17.4 38L12.7 34Z"></path>
</svg>
<span class="radio-stickers__sticker" aria-hidden="true">
<svg viewBox="0 0 44 44">
<path d="M8 38V19C8 11 14 5.5 22 5.5C30 5.5 36 11 36 19V38L31.3 34L26.6 38L22 34L17.4 38L12.7 34Z"></path>
<path d="M17 17.5V20.5M27 17.5V20.5"></path>
</svg>
<span class="radio-stickers__shine"></span>
</span>
</label>
<label class="radio-stickers__slot" style="left:184px;top:114px;--tx:-148px;--ty:-52px;--rot:6deg">
<input class="radio-stickers__input" type="radio" aria-label="Flower" name="radio-stickers" value="flower"/>
<svg class="radio-stickers__ghost" viewBox="0 0 44 44" aria-hidden="true">
<path d="M22 7.5A6.5 6.5 0 0 1 28.1 16.3A6.5 6.5 0 0 1 32.3 27.5A6.5 6.5 0 0 1 22 33.8A6.5 6.5 0 0 1 11.7 27.5A6.5 6.5 0 0 1 15.9 16.3A6.5 6.5 0 0 1 22 7.5Z"></path>
</svg>
<span class="radio-stickers__sticker" aria-hidden="true">
<svg viewBox="0 0 44 44">
<path d="M22 7.5A6.5 6.5 0 0 1 28.1 16.3A6.5 6.5 0 0 1 32.3 27.5A6.5 6.5 0 0 1 22 33.8A6.5 6.5 0 0 1 11.7 27.5A6.5 6.5 0 0 1 15.9 16.3A6.5 6.5 0 0 1 22 7.5Z"></path>
<path d="M22 18.5A3.2 3.2 0 1 1 21.9 18.5Z"></path>
</svg>
<span class="radio-stickers__shine"></span>
</span>
</label>
</div>
</div>CSS
.radio-stickers { --vinyl: light-dark(#373a40, #232529); --cut: light-dark(#fbfcfd, #f1f2f3); --sheet: light-dark(#ebedf0, #c6c8cd); --lid: light-dark(#dbdde1, #2e3035); position: relative; width: 254px; height: 196px; perspective: 600px; }
.radio-stickers__laptop { position: absolute; left: 2px; top: 38px; width: 112px; height: 80px; border-radius: 9px; background: radial-gradient(130% 90% at 30% 10%, color-mix(in oklab, var(--lid) 80%, light-dark(#373a40, #f1f2f3)), var(--lid) 60%); box-shadow: inset 0 0 0 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 12%, transparent), 0 12px 18px -8px light-dark(rgba(0, 0, 0, 0.17), rgba(0, 0, 0, 0.55)); }
.radio-stickers__laptop::after { content: ''; position: absolute; left: 50%; bottom: -6px; width: 124px; height: 6px; translate: -50% 0; border-radius: 0 0 6px 6px; background: color-mix(in oklab, var(--lid) 80%, light-dark(#fbfcfd, #121417)); }
.radio-stickers__read { position: absolute; left: 2px; top: 136px; width: 112px; display: grid; color: light-dark(#373a40, #f1f2f3); font: 900 13px/1 ui-rounded, 'SF Pro Rounded', 'Arial Rounded MT Bold', system-ui, sans-serif; letter-spacing: 0.04em; text-align: center; text-transform: uppercase; }
.radio-stickers__read > span { grid-area: 1 / 1; opacity: 0; transform: translateY(4px); transition: opacity 0.2s, transform 0.3s; }
.radio-stickers__read small { display: block; margin-top: 4px; color: light-dark(#6d7179, #7d8087); font-size: 9px; font-weight: 600; letter-spacing: 0.06em; text-transform: none; }
.radio-stickers:has(.radio-stickers__slot:nth-child(1) input:checked) .radio-stickers__read > :nth-child(1), .radio-stickers:has(.radio-stickers__slot:nth-child(2) input:checked) .radio-stickers__read > :nth-child(2), .radio-stickers:has(.radio-stickers__slot:nth-child(3) input:checked) .radio-stickers__read > :nth-child(3), .radio-stickers:has(.radio-stickers__slot:nth-child(4) input:checked) .radio-stickers__read > :nth-child(4), .radio-stickers:has(.radio-stickers__slot:nth-child(5) input:checked) .radio-stickers__read > :nth-child(5), .radio-stickers:has(.radio-stickers__slot:nth-child(6) input:checked) .radio-stickers__read > :nth-child(6) { opacity: 1; transform: none; transition-delay: 0.55s; }
.radio-stickers__sheet { position: absolute; left: 128px; top: 12px; width: 108px; height: 150px; border-radius: 6px; background: linear-gradient(155deg, color-mix(in oklab, var(--sheet) 80%, light-dark(#fbfcfd, #121417)), var(--sheet) 40%); box-shadow: 0 1px 2px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.25)), 0 10px 18px -8px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); transform: rotate(1.2deg); }
.radio-stickers__slot { position: absolute; width: 44px; height: 44px; cursor: pointer; }
.radio-stickers__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.radio-stickers__ghost { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; fill: none; stroke: color-mix(in oklab, var(--sheet) 60%, light-dark(#fbfcfd, #121417)); stroke-width: 1; stroke-dasharray: 2 2; }
/* The sticker: peeled by a corner, flown over, slapped down on the lid. */
.radio-stickers__sticker { position: absolute; inset: 0; transform-origin: 70% 70%; transition: transform 0.55s cubic-bezier(0.45, 0, 0.2, 1); filter: drop-shadow(0 1px 1px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); }
.radio-stickers__sticker svg { display: block; width: 100%; height: 100%; overflow: visible; }
.radio-stickers__sticker path:first-child { fill: var(--vinyl); stroke: var(--cut); stroke-width: 4.5; stroke-linejoin: round; paint-order: stroke; }
.radio-stickers__sticker path + path { fill: none; stroke: var(--cut); stroke-width: 2.6; stroke-linecap: round; }
.radio-stickers__slot:hover .radio-stickers__sticker { transform: rotate(-4deg) scale(1.06); }
.radio-stickers__slot:has(input:focus-visible) .radio-stickers__ghost { stroke: light-dark(#373a40, #f1f2f3); stroke-dasharray: none; stroke-width: 2; }
.radio-stickers__slot:has(input:checked) { z-index: 2; }
.radio-stickers__slot:has(input:checked) .radio-stickers__sticker { transform: translate(var(--tx), var(--ty)) rotate(var(--rot)) scale(1.32); transition: none; animation: radio-stickers-peel 0.85s cubic-bezier(0.4, 0, 0.2, 1); }
@keyframes radio-stickers-peel {
0% { transform: none; }
22% { transform: translate(-3px, -5px) rotate3d(1, -1, 0, 38deg) scale(1.06); filter: drop-shadow(0 6px 5px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3))); }
68% { transform: translate(calc(var(--tx) + 4px), calc(var(--ty) - 16px)) rotate(var(--rot)) scale(1.48); filter: drop-shadow(0 16px 12px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3))); }
86% { transform: translate(var(--tx), var(--ty)) rotate(var(--rot)) scale(1.24); }
100% { transform: translate(var(--tx), var(--ty)) rotate(var(--rot)) scale(1.32); }
}
.radio-stickers__slot:has(input:checked) .radio-stickers__shine { animation: radio-stickers-shine 0.6s ease-out 0.8s; }
.radio-stickers__shine { position: absolute; inset: 0; border-radius: 50%; background: linear-gradient(115deg, transparent 35%, color-mix(in oklab, var(--cut) 55%, transparent) 50%, transparent 65%) -60px 0 / 200% 100% no-repeat; opacity: 0; pointer-events: none; mix-blend-mode: overlay; }
@keyframes radio-stickers-shine { 0% { opacity: 1; background-position: -60px 0; } 100% { opacity: 1; background-position: 60px 0; } }
@media (prefers-reduced-motion: reduce) {
.radio-stickers * { animation: none !important; transition: none !important; }
}Goal: Create a "Stickers" radio.
Visual behavior: Pick a badge from a sticker sheet: it peels up by its corner, flies over and slaps onto the laptop with a glint, and the old one goes back to its spot on the sheet.
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 sticker sits in its slot and knows the offset to the laptop lid as two variables. When its input is checked a keyframe peels it with a rotate3d tilt, lifts it toward you on the way over and squashes it on landing; when unchecked, a plain transition takes it home.
- The die-cut white margin is the SVG stroke painted under the fill with paint-order, and a dashed outline marks the empty slot on the backing.
Accessibility: A role="radiogroup" labelled "Badge" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. Each input is named by its sticker and focus outlines the empty slot; the name under the laptop is aria-hidden. Under prefers-reduced-motion the sticker moves at once.
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 "Stickers" radio.
Visual behavior: Pick a badge from a sticker sheet: it peels up by its corner, flies over and slaps onto the laptop with a glint, and the old one goes back to its spot on the sheet.
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 sticker sits in its slot and knows the offset to the laptop lid as two variables. When its input is checked a keyframe peels it with a rotate3d tilt, lifts it toward you on the way over and squashes it on landing; when unchecked, a plain transition takes it home.
- The die-cut white margin is the SVG stroke painted under the fill with paint-order, and a dashed outline marks the empty slot on the backing.
Accessibility: A role="radiogroup" labelled "Badge" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. Each input is named by its sticker and focus outlines the empty slot; the name under the laptop is aria-hidden. Under prefers-reduced-motion the sticker moves at once.
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
Pick a badge from a sticker sheet: it peels up by its corner, flies over and slaps onto the laptop with a glint, and the old one goes back to its spot on the sheet.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, keyframe, 3d
- Techniques
- keyframes, css variables, 3d transform, css transition, svg
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A role="radiogroup" labelled "Badge" around native radio inputs sharing one name, so the arrow keys move the choice and Tab enters the group once. Each input is named by its sticker and focus outlines the empty slot; the name under the laptop is aria-hidden. Under prefers-reduced-motion the sticker moves at once.