Cassette
const css = `
@keyframes toggle-cassette-spin { to { transform: rotate(360deg); } }
@keyframes toggle-cassette-feed { from { box-shadow: 0 0 0 5px light-dark(#6d7179, #7d8087); } to { box-shadow: 0 0 0 1.5px light-dark(#6d7179, #7d8087); } }
@keyframes toggle-cassette-take { from { box-shadow: 0 0 0 1.5px light-dark(#6d7179, #7d8087); } to { box-shadow: 0 0 0 5px light-dark(#6d7179, #7d8087); } }
.toggle-cassette { position: relative; display: inline-flex; align-items: center; gap: 12px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
.toggle-cassette__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-cassette__track {
position: relative;
width: 84px;
height: 54px;
border-radius: 7px;
background: light-dark(#ebedf0, #232529);
box-shadow: inset 0 0 0 1px light-dark(#cbced3, #393c41);
}
.toggle-cassette__input:focus-visible + .toggle-cassette__track { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
.toggle-cassette__sticker { position: absolute; left: 7px; right: 7px; top: 5px; height: 9px; border-radius: 2px; background: repeating-linear-gradient(light-dark(#dbdde1, #2e3035) 0 2px, transparent 2px 4px); }
.toggle-cassette__window { position: absolute; left: 13px; right: 13px; top: 17px; height: 20px; border-radius: 10px; background: light-dark(#fbfcfd, #121417); }
.toggle-cassette__tape { position: absolute; left: 12px; right: 12px; bottom: 3px; height: 1px; background: light-dark(#cbced3, #393c41); }
.toggle-cassette__reel {
position: absolute;
top: 4px;
width: 12px;
height: 12px;
border-radius: 50%;
background: conic-gradient(light-dark(#50535b, #c6c8cd) 0 25deg, light-dark(#dbdde1, #2e3035) 25deg 120deg, light-dark(#50535b, #c6c8cd) 120deg 145deg, light-dark(#dbdde1, #2e3035) 145deg 240deg, light-dark(#50535b, #c6c8cd) 240deg 265deg, light-dark(#dbdde1, #2e3035) 265deg);
animation: toggle-cassette-spin 1.1s linear infinite paused;
}
.toggle-cassette__reel--a { left: 4px; box-shadow: 0 0 0 5px light-dark(#6d7179, #7d8087); }
.toggle-cassette__reel--b { right: 4px; box-shadow: 0 0 0 1.5px light-dark(#6d7179, #7d8087); }
.toggle-cassette__holes { position: absolute; left: 22px; right: 22px; bottom: 5px; display: flex; justify-content: space-between; }
.toggle-cassette__holes i { width: 5px; height: 5px; border-radius: 50%; background: light-dark(#fbfcfd, #121417); }
.toggle-cassette__lamp { position: absolute; right: 6px; bottom: 5px; width: 5px; height: 5px; border-radius: 50%; background: light-dark(#cbced3, #393c41); transition: background-color 0.2s, box-shadow 0.2s; }
.toggle-cassette__input:checked + .toggle-cassette__track .toggle-cassette__reel { animation-play-state: running; }
/* Tape winds from one spool onto the other while it plays. */
.toggle-cassette__input:checked + .toggle-cassette__track .toggle-cassette__reel--a { animation: toggle-cassette-spin 1.1s linear infinite, toggle-cassette-feed 8s linear infinite alternate; }
.toggle-cassette__input:checked + .toggle-cassette__track .toggle-cassette__reel--b { animation: toggle-cassette-spin 1.1s linear infinite, toggle-cassette-take 8s linear infinite alternate; }
.toggle-cassette__input:checked + .toggle-cassette__track .toggle-cassette__lamp { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 5px light-dark(#42454c, #e5e6e9); }
@media (prefers-reduced-motion: reduce) {
.toggle-cassette__input:checked + .toggle-cassette__track .toggle-cassette__reel { animation: none; }
}
`;
export function CassetteToggle() {
return (
<>
<style>
{css}
</style>
<label className="toggle-cassette">
<input className="toggle-cassette__input" type="checkbox" role="switch" />
<span className="toggle-cassette__track" aria-hidden="true">
<span className="toggle-cassette__sticker" />
<span className="toggle-cassette__window">
<span className="toggle-cassette__tape" />
<span className="toggle-cassette__reel toggle-cassette__reel--a" />
<span className="toggle-cassette__reel toggle-cassette__reel--b" />
</span>
<span className="toggle-cassette__holes">
<i />
<i />
</span>
<span className="toggle-cassette__lamp" />
</span>
Playback
</label>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<label class="toggle-cassette"><input class="toggle-cassette__input" type="checkbox" role="switch"/><span class="toggle-cassette__track" aria-hidden="true"><span class="toggle-cassette__sticker"></span><span class="toggle-cassette__window"><span class="toggle-cassette__tape"></span><span class="toggle-cassette__reel toggle-cassette__reel--a"></span><span class="toggle-cassette__reel toggle-cassette__reel--b"></span></span><span class="toggle-cassette__holes"><i></i><i></i></span><span class="toggle-cassette__lamp"></span></span>Playback</label>@keyframes toggle-cassette-spin { to { transform: rotate(360deg); } }
@keyframes toggle-cassette-feed { from { box-shadow: 0 0 0 5px light-dark(#6d7179, #7d8087); } to { box-shadow: 0 0 0 1.5px light-dark(#6d7179, #7d8087); } }
@keyframes toggle-cassette-take { from { box-shadow: 0 0 0 1.5px light-dark(#6d7179, #7d8087); } to { box-shadow: 0 0 0 5px light-dark(#6d7179, #7d8087); } }
.toggle-cassette { position: relative; display: inline-flex; align-items: center; gap: 12px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
.toggle-cassette__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-cassette__track {
position: relative;
width: 84px;
height: 54px;
border-radius: 7px;
background: light-dark(#ebedf0, #232529);
box-shadow: inset 0 0 0 1px light-dark(#cbced3, #393c41);
}
.toggle-cassette__input:focus-visible + .toggle-cassette__track { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
.toggle-cassette__sticker { position: absolute; left: 7px; right: 7px; top: 5px; height: 9px; border-radius: 2px; background: repeating-linear-gradient(light-dark(#dbdde1, #2e3035) 0 2px, transparent 2px 4px); }
.toggle-cassette__window { position: absolute; left: 13px; right: 13px; top: 17px; height: 20px; border-radius: 10px; background: light-dark(#fbfcfd, #121417); }
.toggle-cassette__tape { position: absolute; left: 12px; right: 12px; bottom: 3px; height: 1px; background: light-dark(#cbced3, #393c41); }
.toggle-cassette__reel {
position: absolute;
top: 4px;
width: 12px;
height: 12px;
border-radius: 50%;
background: conic-gradient(light-dark(#50535b, #c6c8cd) 0 25deg, light-dark(#dbdde1, #2e3035) 25deg 120deg, light-dark(#50535b, #c6c8cd) 120deg 145deg, light-dark(#dbdde1, #2e3035) 145deg 240deg, light-dark(#50535b, #c6c8cd) 240deg 265deg, light-dark(#dbdde1, #2e3035) 265deg);
animation: toggle-cassette-spin 1.1s linear infinite paused;
}
.toggle-cassette__reel--a { left: 4px; box-shadow: 0 0 0 5px light-dark(#6d7179, #7d8087); }
.toggle-cassette__reel--b { right: 4px; box-shadow: 0 0 0 1.5px light-dark(#6d7179, #7d8087); }
.toggle-cassette__holes { position: absolute; left: 22px; right: 22px; bottom: 5px; display: flex; justify-content: space-between; }
.toggle-cassette__holes i { width: 5px; height: 5px; border-radius: 50%; background: light-dark(#fbfcfd, #121417); }
.toggle-cassette__lamp { position: absolute; right: 6px; bottom: 5px; width: 5px; height: 5px; border-radius: 50%; background: light-dark(#cbced3, #393c41); transition: background-color 0.2s, box-shadow 0.2s; }
.toggle-cassette__input:checked + .toggle-cassette__track .toggle-cassette__reel { animation-play-state: running; }
/* Tape winds from one spool onto the other while it plays. */
.toggle-cassette__input:checked + .toggle-cassette__track .toggle-cassette__reel--a { animation: toggle-cassette-spin 1.1s linear infinite, toggle-cassette-feed 8s linear infinite alternate; }
.toggle-cassette__input:checked + .toggle-cassette__track .toggle-cassette__reel--b { animation: toggle-cassette-spin 1.1s linear infinite, toggle-cassette-take 8s linear infinite alternate; }
.toggle-cassette__input:checked + .toggle-cassette__track .toggle-cassette__lamp { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 5px light-dark(#42454c, #e5e6e9); }
@media (prefers-reduced-motion: reduce) {
.toggle-cassette__input:checked + .toggle-cassette__track .toggle-cassette__reel { animation: none; }
}Goal: Create a "Cassette" toggle.
Visual behavior: A playback switch shaped like a cassette: on, the reels spin and tape slowly winds from one spool to the other, and the play lamp lights.
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 reels are conic-gradient spokes with a box-shadow spread for the wound tape; they spin on a loop that is paused off and running on.
- A second, slow alternate keyframe on each reel shrinks one spread while growing the other, so the tape transfers while it plays.
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 reels stay 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 "Cassette" toggle.
Visual behavior: A playback switch shaped like a cassette: on, the reels spin and tape slowly winds from one spool to the other, and the play lamp lights.
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 reels are conic-gradient spokes with a box-shadow spread for the wound tape; they spin on a loop that is paused off and running on.
- A second, slow alternate keyframe on each reel shrinks one spread while growing the other, so the tape transfers while it plays.
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 reels stay 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 playback switch shaped like a cassette: on, the reels spin and tape slowly winds from one spool to the other, and the play lamp lights.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- click, state change, continuous
- Techniques
- keyframes, conic gradient
- 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 reels stay still.