№ 011

Cartridge Club

A pixel music console where every song is a cartridge — changing tracks ejects one and drops in the next.

♪ Plays sound — click inside to start
Version
1.0.0
Download
16 KB · 4 files
Added
Oct 26, 2026
Built with
HTML, CSS, JS
Needs
Google Fonts
Tags
music player, pixel, web audio

Usage

Copy cartridge-club.css and cartridge-club.js (and assets/, if there is one) into your project, then:

  new CartridgeClub(element, {
    tracks: [{ id: 'CC-001', title: 'AFTER HOURS', artist: 'MONO CLUB', duration: 222,
               cart: '#EB674C', print: '#212B39', decal: 'assets/decal-sunset.png', song: 0, src? }],
    sound: true,             // generated chiptune and mechanical clicks (Web Audio)
    onChange(track, index) {},
  })

A pixel music console where every song is a cartridge. Changing tracks ejects the current cartridge,
swaps it for the next one and drops that into the slot. The track changes the moment the button is
pressed; the 900 ms swap is only feedback, and a tap during a swap replays it at double speed.
`decal` is an image URL used as a mask (any opaque pixels print), or one of the built-in pixel
decals: 'moon', 'bloom', 'invader'. Without `src`, each track plays a generated chiptune (`song`).

Source

/* Cartridge Club — a pixel music console. Everything is laid out on a 256-unit grid (--u),
   so the console scales as one piece with the width of its container. */

.cc {
  --cc-shell: #e9e2d2;
  --cc-shell-lt: #f4efe4;
  --cc-shell-dk: #d3cab6;
  --cc-shell-dk2: #bfb39c;
  --cc-ink: #212b39;
  --cc-slot: #141b25;
  --cc-coral: #eb674c;
  --cc-coral-dk: #c24f37;
  --cc-screen: #a7bbac;
  --cc-screen-dk: #92a797;
  --cc-shadow: rgba(33, 43, 57, 0.18);
  --cc-bitmap: "Silkscreen", "Press Start 2P", ui-monospace, monospace;
  --cc-mono: "VT323", ui-monospace, monospace;

  container-type: inline-size;
  position: relative;
  width: 100%;
  color: var(--cc-ink);
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.cc *,
.cc *::before,
.cc *::after {
  box-sizing: border-box;
}

.cc-stage {
  --u: calc(100cqw / 256);
  position: relative;
  width: 100%;
  height: calc(var(--u) * 232);
}

/* stepped (pixel) corners; --st is the step size */
.cc-stage::before,
.cc-body,
.cc-body-side,
.cc-body-face,
.cc-slot,
.cc-screen,
.cc-screen-glass,
.cc-key,
.cc-key-face,
.cc-cart,
.cc-cart-face {
  clip-path: polygon(
    0 calc(var(--st) * 2), var(--st) calc(var(--st) * 2), var(--st) var(--st), calc(var(--st) * 2) var(--st), calc(var(--st) * 2) 0,
    calc(100% - var(--st) * 2) 0, calc(100% - var(--st) * 2) var(--st), calc(100% - var(--st)) var(--st), calc(100% - var(--st)) calc(var(--st) * 2), 100% calc(var(--st) * 2),
    100% calc(100% - var(--st) * 2), calc(100% - var(--st)) calc(100% - var(--st) * 2), calc(100% - var(--st)) calc(100% - var(--st)), calc(100% - var(--st) * 2) calc(100% - var(--st)), calc(100% - var(--st) * 2) 100%,
    calc(var(--st) * 2) 100%, calc(var(--st) * 2) calc(100% - var(--st)), var(--st) calc(100% - var(--st)), var(--st) calc(100% - var(--st) * 2), 0 calc(100% - var(--st) * 2)
  );
}

/* ── console body ── */

.cc-body {
  --st: calc(var(--u) * 3);
  position: absolute;
  left: calc(var(--u) * 12);
  top: calc(var(--u) * 76);
  width: calc(var(--u) * 232);
  height: calc(var(--u) * 150);
  background: var(--cc-ink);
}

/* hard shadow: a clip-path would cut off a drop-shadow filter, so the shadow is its own layer */
.cc-stage::before {
  content: "";
  position: absolute;
  left: calc(var(--u) * 17);
  top: calc(var(--u) * 81);
  width: calc(var(--u) * 232);
  height: calc(var(--u) * 150);
  background: var(--cc-shadow);
  --st: calc(var(--u) * 3);
}

.cc-body-side {
  --st: calc(var(--u) * 3);
  position: absolute;
  inset: calc(var(--u) * 3);
  background: var(--cc-shell-dk);
}

.cc-body-face {
  --st: calc(var(--u) * 3);
  position: absolute;
  inset: calc(var(--u) * 3) calc(var(--u) * 3) calc(var(--u) * 9);
  background: var(--cc-shell);
  /* a one-unit highlight along the top and left edges */
  box-shadow: inset calc(var(--u) * 2) calc(var(--u) * 2) 0 var(--cc-shell-lt);
}

.cc-slot {
  --st: calc(var(--u) * 2);
  position: absolute;
  left: calc(var(--u) * 28);
  top: calc(var(--u) * 1);
  width: calc(var(--u) * 176);
  height: calc(var(--u) * 9);
  background: var(--cc-slot);
  box-shadow: inset 0 calc(var(--u) * -2) 0 #2c3747;
}

.cc-brand {
  position: absolute;
  left: calc(var(--u) * 26);
  right: calc(var(--u) * 26);
  top: calc(var(--u) * 13);
  display: flex;
  align-items: center;
  gap: calc(var(--u) * 8);
}

.cc-brand::before,
.cc-brand::after {
  content: "";
  flex: 1;
  height: calc(var(--u) * 1.5);
  background: var(--cc-shell-dk2);
}

.cc-brand span {
  font: 700 calc(var(--u) * 7) / 1 var(--cc-bitmap);
  letter-spacing: 0.16em;
  margin-right: -0.16em;
  white-space: nowrap;
}

/* ── screen ── */

.cc-screen {
  --st: calc(var(--u) * 2);
  position: absolute;
  left: calc(var(--u) * 18);
  top: calc(var(--u) * 25);
  width: calc(var(--u) * 196);
  height: calc(var(--u) * 64);
  background: var(--cc-ink);
}

.cc-screen-glass {
  --st: calc(var(--u) * 1.5);
  position: absolute;
  inset: calc(var(--u) * 3);
  background: var(--cc-screen);
  box-shadow: inset calc(var(--u) * 2) calc(var(--u) * 2) 0 var(--cc-screen-dk);
  overflow: hidden;
}

.cc-count,
.cc-time,
.cc-status {
  position: absolute;
  font: 400 calc(var(--u) * 10) / 1 var(--cc-mono);
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.cc-count {
  right: calc(var(--u) * 8);
  top: calc(var(--u) * 5);
}

.cc-info {
  position: absolute;
  inset: 0;
}

.cc-title {
  position: absolute;
  left: calc(var(--u) * 8);
  top: calc(var(--u) * 6);
  margin: 0;
  font: 700 calc(var(--u) * 12) / 1 var(--cc-bitmap);
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.cc-artist {
  position: absolute;
  left: calc(var(--u) * 8);
  top: calc(var(--u) * 21);
  margin: 0;
  font: 400 calc(var(--u) * 7) / 1 var(--cc-bitmap);
  letter-spacing: 0.06em;
  white-space: nowrap;
}

/* 14 columns of 6 blocks; unlit blocks ghost faintly, as on an LCD */
.cc-vis {
  position: absolute;
  right: calc(var(--u) * 8);
  top: calc(var(--u) * 19);
  display: flex;
  align-items: flex-end;
  gap: calc(var(--u) * 1);
  height: calc(var(--u) * 17);
}

.cc-col {
  --h: 1;
  position: relative;
  width: calc(var(--u) * 3);
  height: 100%;
  background: repeating-linear-gradient(to top, rgba(33, 43, 57, 0.1) 0 calc(var(--u) * 2), transparent 0 calc(var(--u) * 3));
}

.cc-col::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: calc(var(--u) * (var(--h) * 3 - 1));
  background: repeating-linear-gradient(to top, var(--cc-ink) 0 calc(var(--u) * 2), transparent 0 calc(var(--u) * 3));
}

.cc-seek {
  position: absolute;
  left: calc(var(--u) * 8);
  bottom: calc(var(--u) * 7);
  width: calc(var(--u) * 104);
  height: calc(var(--u) * 6);
  border: calc(var(--u) * 1.2) solid var(--cc-ink);
  cursor: pointer;
  touch-action: none;
}

/* generous invisible hit area */
.cc-seek::before {
  content: "";
  position: absolute;
  inset: calc(var(--u) * -6) calc(var(--u) * -3);
}

.cc-seek-fill {
  position: absolute;
  left: calc(var(--u) * 0.8);
  top: calc(var(--u) * 0.8);
  bottom: calc(var(--u) * 0.8);
  max-width: calc(100% - var(--u) * 1.6);
  background: var(--cc-ink);
}

.cc-seek:focus-visible {
  outline: calc(var(--u) * 1) dashed var(--cc-ink);
  outline-offset: calc(var(--u) * 2);
}

.cc-time {
  right: calc(var(--u) * 8);
  bottom: calc(var(--u) * 5);
}

.cc-status {
  inset: 0;
  display: grid;
  place-items: center;
  font-size: calc(var(--u) * 13);
}

.cc-status[hidden] {
  display: none;
}

/* ── keys ── */

.cc-controls {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--u) * 96);
  height: calc(var(--u) * 40);
}

.cc-btn {
  --press: 0;
  --face: var(--cc-shell-lt);
  --depth: var(--cc-shell-dk2);
  position: absolute;
  bottom: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--cc-ink);
  cursor: pointer;
  filter: drop-shadow(calc(var(--u) * 2) calc(var(--u) * (3 - var(--press))) 0 var(--cc-shell-dk2));
}

/* at least 44 px to press, whatever the scale */
.cc-btn::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(100%, 44px);
  height: max(100%, 44px);
  transform: translate(-50%, -50%);
}

.cc-prev,
.cc-next {
  width: calc(var(--u) * 32);
  height: calc(var(--u) * 32);
}

.cc-prev { left: calc(var(--u) * 42); }
.cc-next { left: calc(var(--u) * 158); }

.cc-play {
  --face: var(--cc-coral);
  --depth: var(--cc-coral-dk);
  left: calc(var(--u) * 94);
  width: calc(var(--u) * 44);
  height: calc(var(--u) * 40);
}

.cc-key {
  --st: calc(var(--u) * 2);
  position: absolute;
  inset: 0;
  background: var(--cc-ink);
}

/* the depth band shows below the face; pressing slides the face down over it */
.cc-key::before {
  content: "";
  position: absolute;
  inset: calc(var(--u) * 2);
  background: var(--depth);
}

.cc-key-face {
  --st: calc(var(--u) * 1.5);
  position: absolute;
  inset: calc(var(--u) * 2) calc(var(--u) * 2) calc(var(--u) * 6);
  display: grid;
  place-items: center;
  background: var(--face);
  box-shadow: inset calc(var(--u) * 1.5) calc(var(--u) * 1.5) 0 rgba(255, 255, 255, 0.35);
  transform: translateY(calc(var(--u) * var(--press) * 2));
}

.cc-key-face svg {
  display: block;
  height: calc(var(--u) * 10.5);
  fill: var(--cc-ink);
}

.cc-play .cc-key-face svg {
  height: calc(var(--u) * 16.5);
}

.cc-btn:focus-visible {
  outline: calc(var(--u) * 1.2) dashed var(--cc-ink);
  outline-offset: calc(var(--u) * 3);
}

/* ── cartridges ── */

.cc-well {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: calc(var(--u) * 81);   /* the slot line: cartridges disappear below it */
  clip-path: inset(-100% -200% 0 -200%);   /* free to slide out sideways, cut only at the slot */
  pointer-events: none;
}

.cc-cart {
  --st: calc(var(--u) * 3);
  --cart: var(--cc-coral);
  --print: var(--cc-ink);
  --cart-dk: color-mix(in srgb, var(--cart) 84%, #000);
  --cart-lt: color-mix(in srgb, var(--cart) 82%, #fff);
  position: absolute;
  left: calc(var(--u) * 44);
  top: calc(var(--u) * 33);
  width: calc(var(--u) * 168);
  height: calc(var(--u) * 84);
  background: var(--cc-ink);
}

.cc-cart[hidden] {
  display: none;
}

.cc-cart-face {
  --st: calc(var(--u) * 2);
  position: absolute;
  inset: calc(var(--u) * 2);
  background: var(--cart);
  box-shadow: inset 0 calc(var(--u) * 2) 0 var(--cart-lt);
}

/* side grips: a darker column with three notches on the outer edge */
.cc-grip {
  position: absolute;
  top: 0;
  bottom: 0;
  width: calc(var(--u) * 13);
  background:
    repeating-linear-gradient(to bottom, var(--cc-ink) 0 calc(var(--u) * 2), transparent 0 calc(var(--u) * 6))
      0 calc(var(--u) * 13) / calc(var(--u) * 3) calc(var(--u) * 16) no-repeat,
    var(--cart-dk);
}

.cc-grip-l {
  left: 0;
  border-right: calc(var(--u) * 1.5) solid var(--cc-ink);
}

.cc-grip-r {
  right: 0;
  border-left: calc(var(--u) * 1.5) solid var(--cc-ink);
  transform: scaleX(-1);
}

.cc-label {
  position: absolute;
  left: calc(var(--u) * 19);
  right: calc(var(--u) * 19);
  top: calc(var(--u) * 5);
  height: calc(var(--u) * 40);
  border: calc(var(--u) * 1.5) solid var(--cart-dk);
  color: var(--print);
}

.cc-cart-id,
.cc-cart-title {
  position: absolute;
  left: calc(var(--u) * 5);
  font: 400 calc(var(--u) * 6) / 1 var(--cc-bitmap);
  letter-spacing: 0.08em;
  white-space: nowrap;
}

.cc-cart-id { top: calc(var(--u) * 4); }

.cc-cart-rule {
  position: absolute;
  left: calc(var(--u) * 5);
  top: calc(var(--u) * 12);
  width: calc(var(--u) * 50);
  height: calc(var(--u) * 1.5);
  background: var(--print);
}

.cc-cart-title {
  top: calc(var(--u) * 16);
  font-weight: 700;
}

.cc-decal {
  position: absolute;
  left: calc(var(--u) * 3);
  right: calc(var(--u) * 3);
  bottom: calc(var(--u) * 3);
  aspect-ratio: 96 / 25;
  background: var(--print);
  -webkit-mask: var(--decal) center / 100% 100% no-repeat;
  mask: var(--decal) center / 100% 100% no-repeat;
}

/* grip ridges on the part that lives inside the slot */
.cc-ridges {
  position: absolute;
  left: calc(var(--u) * 22);
  right: calc(var(--u) * 22);
  top: calc(var(--u) * 52);
  height: calc(var(--u) * 18);
  background: repeating-linear-gradient(to bottom, var(--cart-dk) 0 calc(var(--u) * 2), transparent 0 calc(var(--u) * 5));
}
All 4 files in the zip
  • assets/decal-sunset.png
  • cartridge-club.css
  • cartridge-club.js
  • index.html