Back to CSS Neumorphism Vinyl Player CSS + JS
Share
HTML
<section class="nm-vin" aria-label="Vinyl player widget">
  <div class="card">
    <span class="glow" aria-hidden="true"></span>
    <span class="glow2" aria-hidden="true"></span>

    <div class="disc-wrap">
    <div class="disc-outer">
      <div class="disc" id="nm-vin-disc">
        <div class="disc-label" aria-hidden="true">♪</div>
      </div>
      <div class="tonearm-wrap" aria-hidden="true">
        <svg width="70" height="80" viewBox="0 0 70 80">
          <line x1="10" y1="10" x2="50" y2="65" stroke="#888" stroke-width="2.5" stroke-linecap="round"/>
          <circle cx="10" cy="10" r="6" fill="#444" stroke="#666" stroke-width="1"/>
          <circle cx="50" cy="65" r="4" fill="#e0a86c" opacity="0.9"/>
        </svg>
      </div>
    </div>
  </div>

  <div class="track-info">
    <div class="track-title">Midnight Reverie</div>
    <div class="track-artist">Aurelia Voss &middot; Late Hours</div>
  </div>

  <div class="progress-wrap">
    <div class="progress-track">
      <div class="progress-fill" id="nm-vin-fill">
        <span class="progress-thumb" aria-hidden="true"></span>
      </div>
    </div>
    <div class="progress-times">
      <span id="nm-vin-elapsed">1:28</span>
      <span>3:52</span>
    </div>
  </div>

  <div class="eq-wrap paused" id="nm-vin-eq" aria-hidden="true">
    <span class="eq-bar"></span><span class="eq-bar"></span><span class="eq-bar"></span>
    <span class="eq-bar"></span><span class="eq-bar"></span><span class="eq-bar"></span>
    <span class="eq-bar"></span><span class="eq-bar"></span><span class="eq-bar"></span>
    <span class="eq-bar"></span><span class="eq-bar"></span><span class="eq-bar"></span>
    <span class="eq-bar"></span><span class="eq-bar"></span><span class="eq-bar"></span>
    <span class="eq-bar"></span><span class="eq-bar"></span><span class="eq-bar"></span>
  </div>

  <div class="controls">
    <button type="button" class="ctrl-btn sm" aria-label="Shuffle">
      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
        <polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/>
        <polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/>
      </svg>
    </button>
    <button type="button" class="ctrl-btn sm" aria-label="Previous">
      <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
        <polygon points="19,20 9,12 19,4"/><rect x="5" y="4" width="2" height="16"/>
      </svg>
    </button>
    <button type="button" class="ctrl-btn lg" id="nm-vin-play" aria-label="Play / Pause">
      <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
        <polygon points="6,4 20,12 6,20"/>
      </svg>
    </button>
    <button type="button" class="ctrl-btn sm" aria-label="Next">
      <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
        <polygon points="5,4 15,12 5,20"/><rect x="17" y="4" width="2" height="16"/>
      </svg>
    </button>
    <button type="button" class="ctrl-btn sm" aria-label="Loop">
      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
        <polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/>
        <polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>
      </svg>
    </button>
  </div>

  <div class="volume-row">
    <span class="vol-icon" aria-hidden="true">
      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
      </svg>
    </span>
    <div class="vol-track"><div class="vol-fill"></div></div>
    <span class="vol-icon" aria-hidden="true">
      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
        <path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
        <path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
      </svg>
    </span>
  </div>
  </div>
</section>
CSS
/* ─── 02 Vinyl Player — dark-slate dark-neumorphic music widget ───── */
@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@300;400&display=swap');

.nm-vin {
  --nm-vin-bg: #1e2230;
  --nm-vin-neu-dark: #141720;
  --nm-vin-neu-light: #282e40;
  --nm-vin-text-primary: #d0d4e0;
  --nm-vin-text-muted: #606880;
  --nm-vin-accent: #e0a86c;
  --nm-vin-accent2: #7c6cdb;
  --nm-vin-inset: #1b1f2d;

  position: relative;
  width: 100%;
  min-height: 720px;
  background: var(--nm-vin-bg);
  font-family: 'DM Mono', ui-monospace, monospace;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px 16px;
  overflow: hidden;
  box-sizing: border-box;
}

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

/* The inner card holds the dark neumorphic shadow */
.nm-vin .card {
  position: relative;
  width: 100%;
  max-width: 400px;
  background: var(--nm-vin-bg);
  border-radius: 36px;
  padding: 40px 34px 36px;
  box-shadow:
    14px 14px 32px var(--nm-vin-neu-dark),
    -10px -10px 28px var(--nm-vin-neu-light);
  overflow: hidden;
}

/* Glows — contained inside the card */
.nm-vin .glow {
  position: absolute;
  width: 200px;
  height: 200px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(124, 108, 219, 0.08), transparent 70%);
  top: -60px;
  right: -40px;
  pointer-events: none;
}
.nm-vin .glow2 {
  position: absolute;
  width: 160px;
  height: 160px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(224, 168, 108, 0.07), transparent 70%);
  bottom: -40px;
  left: -30px;
  pointer-events: none;
}

/* Vinyl disc */
.nm-vin .disc-wrap {
  display: flex;
  justify-content: center;
  position: relative;
}
.nm-vin .disc-outer {
  width: 200px;
  height: 200px;
  border-radius: 50%;
  background: var(--nm-vin-bg);
  box-shadow:
    10px 10px 24px var(--nm-vin-neu-dark),
    -8px -8px 20px var(--nm-vin-neu-light);
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}
.nm-vin .disc {
  width: 170px;
  height: 170px;
  border-radius: 50%;
  background:
    repeating-radial-gradient(circle at 50% 50%,
      transparent 0px, transparent 3px,
      rgba(255, 255, 255, 0.02) 3px, rgba(255, 255, 255, 0.02) 4px
    ),
    conic-gradient(from 0deg,
      #1a1d2a 0deg, #242838 90deg, #1a1d2a 180deg, #242838 270deg, #1a1d2a 360deg
    );
  box-shadow:
    inset 4px 4px 12px rgba(0, 0, 0, 0.6),
    inset -3px -3px 8px rgba(255, 255, 255, 0.04);
  display: flex;
  align-items: center;
  justify-content: center;
  animation: nm-vin-spin 4s linear infinite;
  animation-play-state: paused;
}
.nm-vin .disc.playing { animation-play-state: running; }
@keyframes nm-vin-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
.nm-vin .disc-label {
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--nm-vin-accent), #c4863a);
  box-shadow:
    inset 2px 2px 6px rgba(0, 0, 0, 0.3),
    inset -1px -1px 3px rgba(255, 255, 255, 0.2);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  color: #1a1d2a;
}

/* Tonearm */
.nm-vin .tonearm-wrap {
  position: absolute;
  top: -8px;
  right: -8px;
  width: 70px;
  height: 80px;
}
.nm-vin .tonearm-wrap svg {
  filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.6));
}

/* Track info */
.nm-vin .track-info { text-align: center; margin-bottom: 22px; }
.nm-vin .track-title {
  font-family: 'DM Serif Display', Georgia, serif;
  font-size: 22px;
  color: var(--nm-vin-text-primary);
  margin-bottom: 5px;
  letter-spacing: -0.3px;
}
.nm-vin .track-artist {
  font-size: 10px;
  font-weight: 300;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: var(--nm-vin-text-muted);
}

/* Progress bar */
.nm-vin .progress-wrap { margin-bottom: 22px; position: relative; }
.nm-vin .progress-track {
  height: 4px;
  border-radius: 2px;
  background: var(--nm-vin-inset);
  box-shadow:
    inset 2px 2px 4px var(--nm-vin-neu-dark),
    inset -1px -1px 3px rgba(255, 255, 255, 0.04);
  position: relative;
  cursor: pointer;
}
.nm-vin .progress-fill {
  height: 100%;
  width: 38%;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--nm-vin-accent2), var(--nm-vin-accent));
  position: relative;
}
.nm-vin .progress-thumb {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--nm-vin-bg);
  box-shadow:
    2px 2px 6px var(--nm-vin-neu-dark),
    -1px -1px 4px var(--nm-vin-neu-light);
  position: absolute;
  right: -6px;
  top: -4px;
}
.nm-vin .progress-times {
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
}
.nm-vin .progress-times span {
  font-size: 9px;
  color: var(--nm-vin-text-muted);
  letter-spacing: 1px;
}

/* EQ bars */
.nm-vin .eq-wrap {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 4px;
  height: 30px;
  margin-bottom: 24px;
}
.nm-vin .eq-bar {
  display: block;
  width: 4px;
  border-radius: 2px;
  background: linear-gradient(to top, var(--nm-vin-accent2), var(--nm-vin-accent));
  animation: nm-vin-eq 0.6s ease-in-out infinite alternate;
  opacity: 0.7;
}
.nm-vin .eq-bar:nth-child(1)  { height: 40%; animation-duration: 0.5s; animation-delay: 0.0s; }
.nm-vin .eq-bar:nth-child(2)  { height: 70%; animation-duration: 0.7s; animation-delay: 0.1s; }
.nm-vin .eq-bar:nth-child(3)  { height: 90%; animation-duration: 0.4s; animation-delay: 0.2s; }
.nm-vin .eq-bar:nth-child(4)  { height: 60%; animation-duration: 0.6s; animation-delay: 0.05s; }
.nm-vin .eq-bar:nth-child(5)  { height: 100%; animation-duration: 0.8s; animation-delay: 0.15s; }
.nm-vin .eq-bar:nth-child(6)  { height: 75%; animation-duration: 0.5s; animation-delay: 0.25s; }
.nm-vin .eq-bar:nth-child(7)  { height: 50%; animation-duration: 0.7s; animation-delay: 0.1s; }
.nm-vin .eq-bar:nth-child(8)  { height: 85%; animation-duration: 0.4s; animation-delay: 0.3s; }
.nm-vin .eq-bar:nth-child(9)  { height: 60%; animation-duration: 0.6s; animation-delay: 0.0s; }
.nm-vin .eq-bar:nth-child(10) { height: 35%; animation-duration: 0.5s; animation-delay: 0.2s; }
.nm-vin .eq-bar:nth-child(11) { height: 80%; animation-duration: 0.9s; animation-delay: 0.05s; }
.nm-vin .eq-bar:nth-child(12) { height: 55%; animation-duration: 0.6s; animation-delay: 0.15s; }
.nm-vin .eq-bar:nth-child(13) { height: 45%; animation-duration: 0.4s; animation-delay: 0.25s; }
.nm-vin .eq-bar:nth-child(14) { height: 70%; animation-duration: 0.7s; animation-delay: 0.1s; }
.nm-vin .eq-bar:nth-child(15) { height: 90%; animation-duration: 0.5s; animation-delay: 0.35s; }
.nm-vin .eq-bar:nth-child(16) { height: 65%; animation-duration: 0.8s; animation-delay: 0.0s; }
.nm-vin .eq-bar:nth-child(17) { height: 40%; animation-duration: 0.6s; animation-delay: 0.2s; }
.nm-vin .eq-bar:nth-child(18) { height: 55%; animation-duration: 0.4s; animation-delay: 0.1s; }
@keyframes nm-vin-eq {
  0%   { transform: scaleY(0.3); }
  100% { transform: scaleY(1); }
}
.nm-vin .eq-wrap.paused .eq-bar { animation-play-state: paused; }

/* Controls */
.nm-vin .controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
}
.nm-vin .ctrl-btn {
  border: none;
  background: var(--nm-vin-bg);
  border-radius: 50%;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--nm-vin-text-muted);
  transition: all 0.15s;
}
.nm-vin .ctrl-btn.sm {
  width: 40px;
  height: 40px;
  box-shadow:
    4px 4px 10px var(--nm-vin-neu-dark),
    -3px -3px 8px var(--nm-vin-neu-light);
  font-size: 12px;
}
.nm-vin .ctrl-btn.sm:active {
  box-shadow:
    inset 2px 2px 6px var(--nm-vin-neu-dark),
    inset -1px -1px 4px var(--nm-vin-neu-light);
  color: var(--nm-vin-accent);
}
.nm-vin .ctrl-btn.lg {
  width: 62px;
  height: 62px;
  box-shadow:
    7px 7px 16px var(--nm-vin-neu-dark),
    -6px -6px 14px var(--nm-vin-neu-light);
  color: var(--nm-vin-accent);
  font-size: 20px;
}
.nm-vin .ctrl-btn.lg:active {
  box-shadow:
    inset 3px 3px 8px var(--nm-vin-neu-dark),
    inset -2px -2px 6px var(--nm-vin-neu-light);
}

/* Volume */
.nm-vin .volume-row {
  display: flex;
  align-items: center;
  gap: 12px;
}
.nm-vin .vol-icon { color: var(--nm-vin-text-muted); font-size: 11px; }
.nm-vin .vol-track {
  flex: 1;
  height: 4px;
  border-radius: 2px;
  background: var(--nm-vin-inset);
  box-shadow:
    inset 2px 2px 4px var(--nm-vin-neu-dark),
    inset -1px -1px 3px rgba(255, 255, 255, 0.04);
  position: relative;
}
.nm-vin .vol-fill {
  height: 100%;
  width: 62%;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--nm-vin-accent2), var(--nm-vin-accent));
}

@media (prefers-reduced-motion: reduce) {
  .nm-vin .disc,
  .nm-vin .eq-bar { animation: none; }
}
JS
(() => {
  const root = document.querySelector('.nm-vin');
  if (!root) return;
  let playing = false;
  const disc = root.querySelector('#nm-vin-disc');
  const eqWrap = root.querySelector('#nm-vin-eq');
  const playBtn = root.querySelector('#nm-vin-play');
  const pFill = root.querySelector('#nm-vin-fill');
  const elapsedEl = root.querySelector('#nm-vin-elapsed');

  let elapsed = 88;
  const total = 232;
  let interval = null;
  const fmt = (s) => Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
  const playIcon = '<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><polygon points="6,4 20,12 6,20"/></svg>';
  const pauseIcon = '<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>';

  if (playBtn) playBtn.addEventListener('click', () => {
    playing = !playing;
    if (disc) disc.classList.toggle('playing', playing);
    if (playing) {
      eqWrap?.classList.remove('paused');
      playBtn.innerHTML = pauseIcon;
      interval = setInterval(() => {
        elapsed = Math.min(elapsed + 1, total);
        if (pFill) pFill.style.width = (elapsed / total * 100) + '%';
        if (elapsedEl) elapsedEl.textContent = fmt(elapsed);
        if (elapsed >= total) { clearInterval(interval); playing = false; }
      }, 1000);
    } else {
      eqWrap?.classList.add('paused');
      clearInterval(interval);
      playBtn.innerHTML = playIcon;
    }
  });
})();