Cassette

No dependencies
Code format
React + Tailwind
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>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/toggle-cassette
Source on GitHub

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.

More toggles

See all
Charge
Circuit
Cradle
Domino