№ 010

Living Sleeve

A record sleeve that is the player — press play and it glides aside to reveal a spinning record.

♪ Plays sound — click inside to start
Version
1.0.0
Download
1.1 MB · 5 files
Added
Oct 26, 2026
Built with
HTML, CSS, JS
Needs
Google Fonts
Tags
music player, vinyl, web audio

Usage

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

  new LivingSleeve(element, {
    tracks: [{ artist, title: ['AFTER', 'HOURS'], side, duration, src? }],  // duration in seconds; src is optional audio
    vinyl: 'assets/vinyl.png',
    paper: 'assets/paper.jpg',
    onChange(track, index) {},
  })

A record sleeve that is the player: press play and the sleeve glides left to reveal a spinning record;
pause and, once the record has stopped, it slides back into the sleeve.
Without `src`, each track plays a small generative lo-fi groove (Web Audio) with vinyl crackle.

Source

/* Living Sleeve — a printed record sleeve that slides open to reveal a spinning record.
 * --ls-s (the sleeve's side length) is set from JS; everything else is sized from it. */

.ls {
  --ls-paper: #f2ede3;
  --ls-ink: #20201e;
  --ls-vermilion: #ef5037;
  --ls-grey: #a49e94;
  --ls-s: 280px;
  position: relative;
  width: 100%;
  max-width: 560px;
  height: calc(var(--ls-s) + var(--ls-s) * 0.17);
  color: var(--ls-ink);
  -webkit-tap-highlight-color: transparent;
}

/* the square the record and sleeve share: centred when closed; when open the record slides right
   while the sleeve slides left, so the opened object stays centred too */
.ls-stage {
  position: absolute;
  left: 50%;
  top: 0;
  width: var(--ls-s);
  height: var(--ls-s);
  transform: translateX(-50%);
  transition: transform 450ms cubic-bezier(0.16, 1, 0.3, 1) 70ms;
}

.ls.is-open .ls-stage { transform: translateX(calc(-50% + var(--ls-s) * 0.25)); }

/* ── the record ── */

.ls-disc {
  position: absolute;
  left: 4.5%;
  top: 4.5%;
  width: 91%;
  height: 91%;
  border-radius: 50%;
  box-shadow: 10px 16px 24px rgba(40, 32, 22, 0.28), 2px 3px 5px rgba(40, 32, 22, 0.25);
  will-change: transform;
}

.ls-vinyl {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

/* printed label text on the vermilion centre (36.75% of the disc), so the rotation reads */
.ls-label {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 36.75%;
  height: 36.75%;
  transform: translate(-50%, -50%);
  pointer-events: none;
}

.ls-label span {
  position: absolute;
  left: 0;
  right: 0;
  text-align: center;
  color: rgba(32, 32, 30, 0.82);
  white-space: nowrap;
}

.ls-label-artist {
  top: 22%;
  font: 600 calc(var(--ls-s) * 0.022) / 1 "Inter", system-ui, sans-serif;
  letter-spacing: 0.2em;
}

.ls-label-title {
  bottom: 22%;
  font: 400 calc(var(--ls-s) * 0.02) / 1 "IBM Plex Mono", ui-monospace, monospace;
  letter-spacing: 0.08em;
}

/* ── the sleeve ── */

.ls-sleeve {
  position: absolute;
  inset: 0;
  border-radius: 2.5px;
  background-color: var(--ls-paper);
  background-size: cover;
  overflow: hidden;
  /* one decisive shadow from the upper-left light, plus a thin printed edge */
  box-shadow:
    0 0 0 0.5px rgba(32, 32, 30, 0.14),
    inset -2px 0 0 rgba(32, 32, 30, 0.06),
    inset 0 -1px 0 rgba(32, 32, 30, 0.06),
    14px 20px 30px -6px rgba(40, 32, 22, 0.34),
    3px 4px 6px rgba(40, 32, 22, 0.18);
  transition: transform 450ms cubic-bezier(0.16, 1, 0.3, 1) 70ms;
  will-change: transform;
}

/* the open edge: a hint of card thickness where the record slides out */
.ls-sleeve::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 4px;
  background: linear-gradient(90deg, rgba(32, 32, 30, 0), rgba(32, 32, 30, 0.08));
  pointer-events: none;
}

.ls.is-open .ls-sleeve { transform: translateX(calc(var(--ls-s) * -0.5)); }

.ls-type {
  position: absolute;
  inset: 0;
}

.ls-type-layer {
  position: absolute;
  inset: 0;
  padding: calc(var(--ls-s) * 0.065);
}

.ls-top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}

.ls-artist {
  font: 500 calc(var(--ls-s) * 0.032) / 1 "Inter", system-ui, sans-serif;
  letter-spacing: 0.24em;
}

.ls-side {
  color: var(--ls-grey);
  font: 400 calc(var(--ls-s) * 0.026) / 1 "IBM Plex Mono", ui-monospace, monospace;
  letter-spacing: 0.06em;
}

.ls-title {
  position: absolute;
  left: calc(var(--ls-s) * 0.058);
  top: calc(var(--ls-s) * 0.25);
  margin: 0;
  display: flex;
  flex-direction: column;
  font: 400 calc(var(--ls-s) * 0.255) / 0.92 "Anton", "Impact", sans-serif;
  letter-spacing: -0.005em;
  text-transform: uppercase;
}

.ls-title em {
  color: var(--ls-vermilion);
  font-style: normal;
}

.ls-meta {
  position: absolute;
  left: calc(var(--ls-s) * 0.065);
  bottom: calc(var(--ls-s) * 0.075);
  display: flex;
  flex-direction: column;
  gap: 0.45em;
  color: var(--ls-grey);
  font: 400 calc(var(--ls-s) * 0.024) / 1 "IBM Plex Mono", ui-monospace, monospace;
  letter-spacing: 0.08em;
}

/* ── controls ── */

.ls-play {
  position: absolute;
  right: calc(var(--ls-s) * 0.06);
  bottom: calc(var(--ls-s) * 0.065);
  width: calc(var(--ls-s) * 0.13);
  height: calc(var(--ls-s) * 0.13);
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--ls-ink);
  color: var(--ls-paper);
  cursor: pointer;
  transition: transform 90ms ease-out, background-color 0.2s;
}

.ls-play svg { width: 46%; height: 46%; fill: currentColor; }
.ls-play:hover { background: #3a3935; }
.ls-play.is-press, .ls-play:active { transform: scale(0.9); }
.ls-play:focus-visible { outline: 2px solid var(--ls-vermilion); outline-offset: 3px; }

/* thin progress track along the sleeve's bottom edge; the tall hit area keeps it touchable */
.ls-seek {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: calc(var(--ls-s) * 0.045);
  cursor: pointer;
  touch-action: none;
}

.ls-seek::before,
.ls-seek-fill {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  transition: height 0.15s;
}

.ls-seek::before { background: rgba(164, 158, 148, 0.35); }
.ls-seek-fill { background: var(--ls-vermilion); transform-origin: left; transform: scaleX(0); }
.ls-seek:hover::before, .ls-seek:hover .ls-seek-fill, .ls-seek.is-seeking::before, .ls-seek.is-seeking .ls-seek-fill { height: 4px; }
.ls-seek:focus-visible { outline: none; }
.ls-seek:focus-visible::before { background: rgba(239, 80, 55, 0.35); }

/* strip revealed under the exposed record: previous, elapsed time, next */
.ls-strip {
  position: absolute;
  right: 0;
  top: calc(var(--ls-s) * 1.05);
  width: calc(var(--ls-s) * 0.62);
  height: calc(var(--ls-s) * 0.1);
  display: flex;
  align-items: center;
  justify-content: space-between;
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  transition: opacity 0.3s ease 0.25s, transform 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.25s;
}

.ls.is-open .ls-strip { opacity: 1; transform: none; pointer-events: auto; }

.ls-btn {
  width: calc(var(--ls-s) * 0.1);
  height: calc(var(--ls-s) * 0.1);
  min-width: 36px;
  min-height: 36px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--ls-ink);
  cursor: pointer;
  transition: background-color 0.2s, color 0.2s;
}

.ls-btn svg { width: 46%; height: 46%; fill: currentColor; }
.ls-btn:hover { background: rgba(32, 32, 30, 0.06); }
.ls-btn:active { background: rgba(32, 32, 30, 0.12); }
.ls-btn:focus-visible { outline: 2px solid var(--ls-vermilion); outline-offset: 1px; }

.ls-time {
  color: #6e6a62;
  font: 400 calc(var(--ls-s) * 0.032) / 1 "IBM Plex Mono", ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
}

@media (prefers-reduced-motion: reduce) {
  .ls-stage, .ls-sleeve, .ls-strip, .ls-play { transition: none; }
}
All 5 files in the zip
  • assets/paper.jpg
  • assets/vinyl.png
  • index.html
  • living-sleeve.css
  • living-sleeve.js