Back to CSS Neumorphism Audio Control Suite CSS + JS
Share
HTML
<section class="nm-aud" aria-label="Audio control suite">
  <div class="card">

    <div class="track-header">
      <div>
        <div class="track-title">Ember &amp; Ash</div>
        <div class="track-sub">SOLARIS &amp; THE ECHO · LIMINAL HOURS</div>
      </div>
      <div class="track-bpm">BPM <span>124</span></div>
    </div>

    <div class="waveform-row" data-nm-aud-wave aria-hidden="true"></div>

    <div class="scrubber-wrap">
      <div class="scrubber-label-row">
        <span data-nm-aud-time-now>1:42</span>
        <span>3:47</span>
      </div>
      <div class="scrubber-track">
        <div class="scrubber-glow" data-nm-aud-glow></div>
        <div class="scrubber-fill" data-nm-aud-fill></div>
        <div class="scrubber-thumb" data-nm-aud-thumb></div>
        <input type="range" class="scrubber" data-nm-aud-scrub min="0" max="100" value="38" aria-label="Track progress" />
      </div>
    </div>

    <div class="main-controls">

      <div class="play-controls">
        <button type="button" class="ctrl ctrl-sm" aria-label="Shuffle">⇄</button>
        <button type="button" class="ctrl ctrl-md" aria-label="Previous">⏮</button>
        <button type="button" class="ctrl ctrl-play" data-nm-aud-play aria-label="Play / Pause">▶</button>
        <button type="button" class="ctrl ctrl-md" aria-label="Next">⏭</button>
        <button type="button" class="ctrl ctrl-sm" aria-label="Loop">↻</button>
      </div>

      <div class="vol-knob-wrap">
        <div class="vol-knob" data-nm-aud-volknob>
          <svg class="vol-tick-svg" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" data-nm-aud-volticks aria-hidden="true"></svg>
          <div class="vol-knob-inner"></div>
          <div class="vol-knob-dot" data-nm-aud-voldot></div>
        </div>
        <div class="mini-lbl">Volume</div>
        <div class="vol-val" data-nm-aud-volval>72</div>
      </div>

      <div class="eq-wrap" data-nm-aud-eq></div>

    </div>

    <div class="knobs-row" data-nm-aud-knobs></div>

  </div>
</section>
CSS
/* ─── 05 Audio Control Suite — warm parchment audio console ───────── */
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,500;0,700;1,400&family=Space+Mono:wght@400;700&display=swap');

.nm-aud {
  --nm-aud-bg:  #f0e6d3;
  --nm-aud-sd:  #d0c0a0;
  --nm-aud-sl:  #fffaf0;
  --nm-aud-ib:  #e8ddc8;
  --nm-aud-acc: #c8783a;
  --nm-aud-ac2: #e0a858;
  --nm-aud-ac3: #8a4a20;
  --nm-aud-txt: #7a6a58;
  --nm-aud-txt2:#2a1e10;

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

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

.nm-aud .card {
  position: relative;
  width: 100%;
  max-width: 860px;
  background: var(--nm-aud-bg);
  border-radius: 44px;
  padding: 44px 40px 38px;
  box-shadow: 24px 24px 56px var(--nm-aud-sd), -24px -24px 56px var(--nm-aud-sl);
  display: flex;
  flex-direction: column;
  gap: 30px;
}

/* Track header */
.nm-aud .track-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.nm-aud .track-title {
  font-family: 'Playfair Display', Georgia, serif;
  font-size: 26px;
  font-weight: 700;
  color: var(--nm-aud-txt2);
  letter-spacing: -0.5px;
}
.nm-aud .track-sub {
  font-size: 11px;
  color: var(--nm-aud-txt);
  margin-top: 4px;
  letter-spacing: 0.5px;
}
.nm-aud .track-bpm {
  background: var(--nm-aud-bg);
  border-radius: 12px;
  padding: 10px 18px;
  box-shadow: inset 5px 5px 10px var(--nm-aud-sd), inset -5px -5px 10px var(--nm-aud-sl);
  font-size: 11px;
  color: var(--nm-aud-txt);
}
.nm-aud .track-bpm span {
  color: var(--nm-aud-acc);
  font-weight: 700;
  font-size: 15px;
}

/* Waveform */
.nm-aud .waveform-row {
  display: flex;
  align-items: center;
  gap: 3px;
  height: 36px;
  margin-bottom: 6px;
}
.nm-aud .wf-bar {
  flex: 1;
  border-radius: 2px;
  background: linear-gradient(to top, var(--nm-aud-sd), var(--nm-aud-ib));
  opacity: 0.45;
  min-height: 3px;
}
.nm-aud .wf-bar.played {
  background: linear-gradient(to top, var(--nm-aud-acc), var(--nm-aud-ac2));
  opacity: 0.7;
}

/* Scrubber */
.nm-aud .scrubber-wrap { position: relative; }
.nm-aud .scrubber-label-row {
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  color: var(--nm-aud-txt);
  margin-bottom: 10px;
  letter-spacing: 0.5px;
}
.nm-aud .scrubber-track {
  width: 100%;
  height: 10px;
  border-radius: 5px;
  background: var(--nm-aud-ib);
  box-shadow: inset 5px 5px 10px var(--nm-aud-sd), inset -5px -5px 10px var(--nm-aud-sl);
  position: relative;
  cursor: pointer;
}
.nm-aud .scrubber-fill {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 38%;
  border-radius: 5px;
  background: linear-gradient(90deg, var(--nm-aud-ac3), var(--nm-aud-acc), var(--nm-aud-ac2));
  pointer-events: none;
}
.nm-aud .scrubber-glow {
  position: absolute;
  top: -4px;
  left: 0;
  height: 18px;
  width: 38%;
  border-radius: 9px;
  background: linear-gradient(90deg, transparent, rgba(200, 120, 58, 0.3));
  filter: blur(6px);
  pointer-events: none;
}
.nm-aud .scrubber {
  position: absolute;
  top: -4px;
  left: 0;
  width: 100%;
  height: 18px;
  opacity: 0;
  cursor: pointer;
  margin: 0;
  z-index: 10;
}
.nm-aud .scrubber-thumb {
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--nm-aud-bg);
  box-shadow: 5px 5px 12px var(--nm-aud-sd), -5px -5px 12px var(--nm-aud-sl);
  left: 38%;
  pointer-events: none;
  transition: box-shadow 0.15s;
}
.nm-aud .scrubber-thumb::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: linear-gradient(145deg, var(--nm-aud-acc), var(--nm-aud-ac2));
  box-shadow: 0 0 6px rgba(200, 120, 58, 0.5);
}

/* Main controls grid */
.nm-aud .main-controls {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 28px;
  align-items: center;
}

/* Play controls */
.nm-aud .play-controls {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: flex-start;
  flex-wrap: wrap;
}
.nm-aud .ctrl {
  background: var(--nm-aud-bg);
  border: none;
  cursor: pointer;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 7px 7px 15px var(--nm-aud-sd), -7px -7px 15px var(--nm-aud-sl);
  transition: all 0.15s ease;
  color: var(--nm-aud-txt2);
  font-size: 16px;
}
.nm-aud .ctrl:active {
  box-shadow: inset 4px 4px 10px var(--nm-aud-sd), inset -4px -4px 10px var(--nm-aud-sl);
}
.nm-aud .ctrl:hover { transform: scale(1.05); }
.nm-aud .ctrl-sm { width: 42px; height: 42px; }
.nm-aud .ctrl-md { width: 52px; height: 52px; font-size: 18px; }
.nm-aud .ctrl-play {
  width: 66px;
  height: 66px;
  font-size: 22px;
  background: linear-gradient(145deg, var(--nm-aud-acc), var(--nm-aud-ac3));
  color: #fffaf0;
  box-shadow: 10px 10px 22px rgba(200, 120, 58, 0.4), -6px -6px 16px var(--nm-aud-sl);
}
.nm-aud .ctrl-play:hover { transform: scale(1.05); }
.nm-aud .ctrl-play:active {
  box-shadow: inset 6px 6px 14px rgba(0, 0, 0, 0.2);
  transform: scale(0.97);
}

/* Volume knob */
.nm-aud .vol-knob-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.nm-aud .vol-knob {
  width: 110px;
  height: 110px;
  border-radius: 50%;
  background: var(--nm-aud-bg);
  box-shadow: 14px 14px 32px var(--nm-aud-sd), -14px -14px 32px var(--nm-aud-sl);
  position: relative;
  cursor: grab;
  user-select: none;
}
.nm-aud .vol-knob:active { cursor: grabbing; }
.nm-aud .vol-knob-inner {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 70px;
  height: 70px;
  border-radius: 50%;
  background: var(--nm-aud-ib);
  box-shadow: inset 6px 6px 14px var(--nm-aud-sd), inset -6px -6px 14px var(--nm-aud-sl);
}
.nm-aud .vol-knob-dot {
  position: absolute;
  top: 12px;
  left: 50%;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--nm-aud-acc);
  transform: translateX(-50%);
  transform-origin: 3px 43px;
  box-shadow: 0 0 8px var(--nm-aud-acc);
  transition: transform 0.05s;
}
.nm-aud .vol-tick-svg { position: absolute; inset: 0; }
.nm-aud .mini-lbl {
  font-size: 9px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--nm-aud-txt);
}
.nm-aud .vol-val {
  font-size: 20px;
  font-weight: 700;
  color: var(--nm-aud-txt2);
  margin-top: -6px;
  font-family: 'Space Mono', monospace;
}

/* EQ faders */
.nm-aud .eq-wrap {
  display: flex;
  gap: 14px;
  align-items: flex-end;
  justify-content: flex-end;
}
.nm-aud .fader-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.nm-aud .fader {
  position: relative;
  width: 28px;
  height: 110px;
}
.nm-aud .fader-track {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  width: 8px;
  height: 100%;
  border-radius: 4px;
  background: var(--nm-aud-ib);
  box-shadow: inset 3px 3px 7px var(--nm-aud-sd), inset -3px -3px 7px var(--nm-aud-sl);
}
.nm-aud .fader-fill {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: 8px;
  bottom: 0;
  border-radius: 4px;
  background: linear-gradient(to top, var(--nm-aud-ac3), var(--nm-aud-acc), var(--nm-aud-ac2));
}
.nm-aud .fader-thumb {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: 28px;
  height: 20px;
  border-radius: 6px;
  background: var(--nm-aud-bg);
  box-shadow: 4px 4px 8px var(--nm-aud-sd), -4px -4px 8px var(--nm-aud-sl);
  cursor: ns-resize;
  display: flex;
  align-items: center;
  justify-content: center;
}
.nm-aud .fader-thumb::before {
  content: '';
  display: block;
  width: 14px;
  height: 2px;
  border-radius: 1px;
  background: var(--nm-aud-sd);
  box-shadow: 0 -4px 0 var(--nm-aud-sd), 0 4px 0 var(--nm-aud-sd);
}
.nm-aud .fader-lbl {
  font-size: 8px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--nm-aud-txt);
}
.nm-aud .fader-val {
  font-size: 10px;
  font-weight: 700;
  color: var(--nm-aud-acc);
}

/* Mini knobs */
.nm-aud .knobs-row {
  display: flex;
  gap: 18px;
  align-items: center;
  justify-content: center;
  padding-top: 8px;
  border-top: 1px solid rgba(160, 140, 110, 0.2);
  flex-wrap: wrap;
}
.nm-aud .mini-knob-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.nm-aud .mini-knob {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--nm-aud-bg);
  box-shadow: 9px 9px 20px var(--nm-aud-sd), -9px -9px 20px var(--nm-aud-sl);
  position: relative;
  cursor: grab;
}
.nm-aud .mini-knob:active { cursor: grabbing; }
.nm-aud .mini-knob-inner {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--nm-aud-ib);
  box-shadow: inset 3px 3px 7px var(--nm-aud-sd), inset -3px -3px 7px var(--nm-aud-sl);
}
.nm-aud .mini-knob-dot {
  position: absolute;
  top: 6px;
  left: 50%;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--nm-aud-acc);
  transform: translateX(-50%);
  transform-origin: 2.5px 22px;
  box-shadow: 0 0 5px var(--nm-aud-acc);
}
.nm-aud .mini-val {
  font-size: 11px;
  font-weight: 700;
  color: var(--nm-aud-txt2);
  font-family: 'Space Mono', monospace;
}

@media (max-width: 760px) {
  .nm-aud .card { padding: 32px 22px; }
  .nm-aud .main-controls { grid-template-columns: 1fr; }
  .nm-aud .eq-wrap { justify-content: center; }
  .nm-aud .play-controls { justify-content: center; }
}
JS
(() => {
  const root = document.querySelector('.nm-aud');
  if (!root) return;

  // ─── Waveform ───
  const waveRow = root.querySelector('[data-nm-aud-wave]');
  const waveHeights = [6,10,18,14,24,20,32,28,36,30,40,34,42,38,48,52,44,50,40,46,38,42,32,36,28,30,22,26,18,20,14,16,10,12,8,10,14,18,22,28,32,36,30,24,20,16,12,8,6];
  let scrubberPct = 0.38;
  waveHeights.forEach((h, i) => {
    const bar = document.createElement('div');
    bar.className = 'wf-bar' + (i < waveHeights.length * scrubberPct ? ' played' : '');
    bar.style.height = Math.max(3, h * 0.7) + 'px';
    waveRow.appendChild(bar);
  });

  // ─── Scrubber ───
  const input = root.querySelector('[data-nm-aud-scrub]');
  const fill  = root.querySelector('[data-nm-aud-fill]');
  const glow  = root.querySelector('[data-nm-aud-glow]');
  const thumb = root.querySelector('[data-nm-aud-thumb]');
  const timeNow = root.querySelector('[data-nm-aud-time-now]');
  const totalSec = 227;
  function updateScrubber(val) {
    const pct = val / 100;
    fill.style.width  = (pct * 100) + '%';
    glow.style.width  = (pct * 100) + '%';
    thumb.style.left  = (pct * 100) + '%';
    const sec = Math.round(pct * totalSec);
    timeNow.textContent = Math.floor(sec / 60) + ':' + String(sec % 60).padStart(2, '0');
    waveRow.querySelectorAll('.wf-bar').forEach((b, i) => {
      b.classList.toggle('played', i < waveHeights.length * pct);
    });
  }
  input.addEventListener('input', () => updateScrubber(+input.value));

  // ─── Volume Knob ───
  const volKnob = root.querySelector('[data-nm-aud-volknob]');
  const volDot  = root.querySelector('[data-nm-aud-voldot]');
  const volVal  = root.querySelector('[data-nm-aud-volval]');
  const volTicks = root.querySelector('[data-nm-aud-volticks]');
  let volAngle = -45;
  let volDragging = false, volStartY = 0, volStartAngle = -45;

  // Tick marks
  const svgNS = 'http://www.w3.org/2000/svg';
  for (let t = 0; t <= 20; t++) {
    const frac = t / 20;
    const ang = -225 + frac * 270;
    const rad = ang * Math.PI / 180;
    const r1 = 54, r2 = t % 5 === 0 ? 48 : 51;
    const line = document.createElementNS(svgNS, 'line');
    line.setAttribute('x1', String(60 + r1 * Math.cos(rad)));
    line.setAttribute('y1', String(60 + r1 * Math.sin(rad)));
    line.setAttribute('x2', String(60 + r2 * Math.cos(rad)));
    line.setAttribute('y2', String(60 + r2 * Math.sin(rad)));
    line.setAttribute('stroke', frac < 0.72 ? '#c8783a' : '#d0c0a0');
    line.setAttribute('stroke-width', t % 5 === 0 ? '2' : '1');
    line.setAttribute('stroke-linecap', 'round');
    line.setAttribute('opacity', frac < 0.72 ? '0.8' : '0.4');
    volTicks.appendChild(line);
  }

  function setVol(angle) {
    volAngle = Math.max(-140, Math.min(140, angle));
    volDot.style.transform = 'translateX(-50%) rotate(' + volAngle + 'deg)';
    const pct = Math.round((volAngle + 140) / 280 * 100);
    volVal.textContent = pct;
  }
  setVol(-45);
  volKnob.addEventListener('mousedown', e => {
    volDragging = true; volStartY = e.clientY; volStartAngle = volAngle;
    document.body.style.cursor = 'grabbing';
    e.preventDefault();
  });
  document.addEventListener('mousemove', e => {
    if (!volDragging) return;
    setVol(volStartAngle - (e.clientY - volStartY) * 1.2);
  });
  document.addEventListener('mouseup', () => {
    if (volDragging) { volDragging = false; document.body.style.cursor = ''; }
  });

  // ─── EQ Faders ───
  const eqData = [
    { lbl: '32Hz',  val: 68 },
    { lbl: '250Hz', val: 55 },
    { lbl: '1kHz',  val: 80 },
    { lbl: '4kHz',  val: 62 },
    { lbl: '16kHz', val: 44 },
  ];
  const eqWrap = root.querySelector('[data-nm-aud-eq]');
  eqData.forEach(({ lbl, val }) => {
    const h = 110;
    const fillH = Math.round(val / 100 * h);
    const g = document.createElement('div');
    g.className = 'fader-group';
    g.innerHTML =
      '<div class="fader-val">' + (val >= 50 ? '+' : '') + (val - 50) + '</div>' +
      '<div class="fader">' +
      '  <div class="fader-track"></div>' +
      '  <div class="fader-fill" style="height:' + fillH + 'px"></div>' +
      '  <div class="fader-thumb" style="bottom:' + (fillH - 10) + 'px"></div>' +
      '</div>' +
      '<div class="fader-lbl">' + lbl + '</div>';
    eqWrap.appendChild(g);

    const thumb2 = g.querySelector('.fader-thumb');
    const fill2  = g.querySelector('.fader-fill');
    const valEl  = g.querySelector('.fader-val');
    let dragging = false, startY2 = 0, startPct = val / 100;
    thumb2.addEventListener('mousedown', e => {
      dragging = true; startY2 = e.clientY;
      startPct = parseInt(fill2.style.height, 10) / h;
      document.body.style.cursor = 'ns-resize';
      e.preventDefault();
    });
    document.addEventListener('mousemove', e => {
      if (!dragging) return;
      const delta = (startY2 - e.clientY) / h;
      const pct = Math.max(0, Math.min(1, startPct + delta));
      const fh = Math.round(pct * h);
      fill2.style.height  = fh + 'px';
      thumb2.style.bottom = (fh - 10) + 'px';
      const v = Math.round(pct * 100);
      valEl.textContent = (v >= 50 ? '+' : '') + (v - 50);
    });
    document.addEventListener('mouseup', () => {
      if (dragging) { dragging = false; document.body.style.cursor = ''; }
    });
  });

  // ─── Mini Knobs ───
  const knobData = [
    { lbl: 'Bass',    val: -20 },
    { lbl: 'Treble',  val:  30 },
    { lbl: 'Reverb',  val:  -5 },
    { lbl: 'Delay',   val:  15 },
    { lbl: 'Width',   val:  50 },
    { lbl: 'Drive',   val: -30 },
  ];
  const knobsRow = root.querySelector('[data-nm-aud-knobs]');
  knobData.forEach(({ lbl, val }) => {
    const g = document.createElement('div');
    g.className = 'mini-knob-group';
    g.innerHTML =
      '<div class="mini-knob">' +
      '  <div class="mini-knob-inner"></div>' +
      '  <div class="mini-knob-dot"></div>' +
      '</div>' +
      '<div class="mini-lbl">' + lbl + '</div>' +
      '<div class="mini-val">' + (val > 0 ? '+' : '') + val + '</div>';
    knobsRow.appendChild(g);

    const dot = g.querySelector('.mini-knob-dot');
    const valEl2 = g.querySelector('.mini-val');
    let angle = val;
    dot.style.transform = 'translateX(-50%) rotate(' + angle + 'deg)';
    let drag = false, sy = 0, sa = angle;
    g.querySelector('.mini-knob').addEventListener('mousedown', e => {
      drag = true; sy = e.clientY; sa = angle;
      document.body.style.cursor = 'grabbing';
      e.preventDefault();
    });
    document.addEventListener('mousemove', e => {
      if (!drag) return;
      angle = Math.max(-140, Math.min(140, sa - (e.clientY - sy) * 1.5));
      dot.style.transform = 'translateX(-50%) rotate(' + angle + 'deg)';
      const v = Math.round(angle);
      valEl2.textContent = (v > 0 ? '+' : '') + v;
    });
    document.addEventListener('mouseup', () => {
      if (drag) { drag = false; document.body.style.cursor = ''; }
    });
  });

  // ─── Play toggle ───
  let playing = false;
  const playBtn = root.querySelector('[data-nm-aud-play]');
  playBtn.addEventListener('click', () => {
    playing = !playing;
    playBtn.textContent = playing ? '⏸' : '▶';
  });
})();