Back to CSS 3D Midnight Coverflow CSS + JS
Share
HTML
<section class="cd-cvr" aria-label="Midnight coverflow carousel demo">
  <div class="card">
    <div class="cvr-ground" aria-hidden="true"></div>

    <div class="viewport">
      <div class="stage" data-cd-cvr-stage>

        <div class="cf-card" data-i="0"><div class="cf-card-inner"><div class="card-art">
          <div class="geo geo-1a"></div>
          <div class="geo geo-1b"></div>
          <span class="card-title-sm">001 — Spectra</span>
          <div class="card-title-lg">Void<br />Protocol</div>
          <div class="card-sub">Digital Exploration</div>
        </div></div></div>

        <div class="cf-card" data-i="1"><div class="cf-card-inner"><div class="card-art">
          <div class="geo geo-2a"></div>
          <div class="geo geo-2b"></div>
          <span class="card-title-sm">002 — Azure</span>
          <div class="card-title-lg">Meridian<br />Drift</div>
          <div class="card-sub">Ocean Systems</div>
        </div></div></div>

        <div class="cf-card" data-i="2"><div class="cf-card-inner"><div class="card-art">
          <div class="geo geo-3a"></div>
          <div class="geo geo-3b"></div>
          <span class="card-title-sm">003 — Ember</span>
          <div class="card-title-lg">Solstice<br />Engine</div>
          <div class="card-sub">Heat Architecture</div>
        </div></div></div>

        <div class="cf-card" data-i="3"><div class="cf-card-inner"><div class="card-art">
          <div class="geo geo-4a"></div>
          <div class="geo geo-4b"></div>
          <span class="card-title-sm">004 — Canopy</span>
          <div class="card-title-lg">Biolum<br />Forest</div>
          <div class="card-sub">Living Systems</div>
        </div></div></div>

        <div class="cf-card" data-i="4"><div class="cf-card-inner"><div class="card-art">
          <div class="geo geo-5a"></div>
          <div class="geo geo-5b"></div>
          <span class="card-title-sm">005 — Rosa</span>
          <div class="card-title-lg">Ultraviolet<br />Bloom</div>
          <div class="card-sub">Bio-Digital</div>
        </div></div></div>

        <div class="cf-card" data-i="5"><div class="cf-card-inner"><div class="card-art">
          <div class="geo geo-6a"></div>
          <div class="geo geo-6b"></div>
          <span class="card-title-sm">006 — Sol</span>
          <div class="card-title-lg">Photon<br />Archive</div>
          <div class="card-sub">Light Research</div>
        </div></div></div>

        <div class="cf-card" data-i="6"><div class="cf-card-inner"><div class="card-art">
          <div class="geo geo-7a"></div>
          <div class="geo geo-7b"></div>
          <span class="card-title-sm">007 — Abyss</span>
          <div class="card-title-lg">Deep<br />Current</div>
          <div class="card-sub">Oceanic Studies</div>
        </div></div></div>

      </div>
    </div>

    <div class="nav-dots" data-cd-cvr-dots></div>

    <div class="arrows">
      <button class="arr" type="button" data-cd-cvr-prev aria-label="Previous">&#8592;</button>
      <button class="arr" type="button" data-cd-cvr-next aria-label="Next">&#8594;</button>
    </div>

  </div>
</section>
CSS
/* ─── 02 Midnight Coverflow — 7-card cylindrical carousel ──── */
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;700&family=Unbounded:wght@200;400;700&display=swap');

.cd-cvr {
  --cd-cvr-bg: #060508;

  position: relative;
  width: 100%;
  height: 640px;
  background: var(--cd-cvr-bg);
  font-family: 'Space Grotesk', system-ui, sans-serif;
  overflow: hidden;
  user-select: none;
  box-sizing: border-box;
}

.cd-cvr *,
.cd-cvr *::before,
.cd-cvr *::after { box-sizing: border-box; margin: 0; padding: 0; }

.cd-cvr .card {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.cd-cvr .card::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 90% 60% at 50% 40%, rgba(40,10,80,0.18) 0%, transparent 70%),
    radial-gradient(ellipse 60% 40% at 50% 90%, rgba(180,80,0,0.06) 0%, transparent 70%);
  pointer-events: none;
}

.cd-cvr .cvr-ground {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  height: 45%;
  background: linear-gradient(to top, rgba(5,3,8,0.98) 0%, transparent 100%);
  pointer-events: none;
  z-index: 5;
}

.cd-cvr .viewport {
  perspective: 1000px;
  width: 100%;
  height: 420px;
  position: relative;
  z-index: 1;
}
.cd-cvr .stage {
  width: 100%;
  height: 420px;
  position: relative;
  transform-style: preserve-3d;
}

.cd-cvr .cf-card {
  width: 240px;
  height: 320px;
  position: absolute;
  left: 50%;
  top: 50%;
  margin-left: -120px;
  margin-top: -160px;
  border-radius: 18px;
  overflow: hidden;
  cursor: pointer;
  transform-style: preserve-3d;
  transition: transform 0.65s cubic-bezier(0.23, 1, 0.32, 1),
              opacity 0.65s ease,
              filter 0.65s ease;
  -webkit-box-reflect: below 8px linear-gradient(transparent 75%, rgba(5,3,8,0.55) 100%);
}
.cd-cvr .cf-card-inner {
  width: 100%; height: 100%;
  position: relative;
  overflow: hidden;
  border-radius: 18px;
}
.cd-cvr .card-art {
  width: 100%; height: 100%;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-end;
  padding: 22px;
}
.cd-cvr .card-art::before {
  content: '';
  position: absolute;
  inset: 0;
  background: inherit;
}

.cd-cvr .card-title-sm {
  font-family: 'Space Grotesk', sans-serif;
  font-size: 9px;
  letter-spacing: 3px;
  text-transform: uppercase;
  margin-bottom: 5px;
  position: relative;
  z-index: 1;
}
.cd-cvr .card-title-lg {
  font-family: 'Unbounded', sans-serif;
  font-size: 16px;
  font-weight: 700;
  line-height: 1.25;
  position: relative;
  z-index: 1;
}
.cd-cvr .card-sub {
  font-family: 'Space Grotesk', sans-serif;
  font-size: 10px;
  margin-top: 5px;
  position: relative;
  z-index: 1;
  opacity: 0.65;
}
.cd-cvr .cf-card-inner::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, rgba(255,255,255,0.08) 0%, transparent 40%, rgba(0,0,0,0.2) 100%);
  pointer-events: none;
}

/* Per-card palettes */
.cd-cvr .cf-card[data-i="0"] .card-art { background: linear-gradient(148deg, #0d0020 0%, #2a0058 40%, #1a0040 100%); }
.cd-cvr .cf-card[data-i="0"] .card-art::before { background: radial-gradient(ellipse 70% 60% at 70% 30%, rgba(120,60,255,0.4) 0%, transparent 70%); }
.cd-cvr .cf-card[data-i="0"] .card-title-sm { color: rgba(180,140,255,0.7); }
.cd-cvr .cf-card[data-i="0"] .card-title-lg { color: #e8d8ff; }
.cd-cvr .cf-card[data-i="0"] .card-sub { color: rgba(180,140,255,0.5); }

.cd-cvr .cf-card[data-i="1"] .card-art { background: linear-gradient(148deg, #001a20 0%, #003050 40%, #001828 100%); }
.cd-cvr .cf-card[data-i="1"] .card-art::before { background: radial-gradient(ellipse 70% 60% at 30% 60%, rgba(0,180,255,0.35) 0%, transparent 70%); }
.cd-cvr .cf-card[data-i="1"] .card-title-sm { color: rgba(80,200,255,0.7); }
.cd-cvr .cf-card[data-i="1"] .card-title-lg { color: #c8f0ff; }
.cd-cvr .cf-card[data-i="1"] .card-sub { color: rgba(80,200,255,0.5); }

.cd-cvr .cf-card[data-i="2"] .card-art { background: linear-gradient(148deg, #100400 0%, #300e00 40%, #1e0800 100%); }
.cd-cvr .cf-card[data-i="2"] .card-art::before { background: radial-gradient(ellipse 70% 60% at 60% 40%, rgba(255,100,0,0.4) 0%, transparent 70%); }
.cd-cvr .cf-card[data-i="2"] .card-title-sm { color: rgba(255,160,80,0.7); }
.cd-cvr .cf-card[data-i="2"] .card-title-lg { color: #ffe8c8; }
.cd-cvr .cf-card[data-i="2"] .card-sub { color: rgba(255,160,80,0.5); }

.cd-cvr .cf-card[data-i="3"] .card-art { background: linear-gradient(148deg, #001800 0%, #003000 40%, #001a00 100%); }
.cd-cvr .cf-card[data-i="3"] .card-art::before { background: radial-gradient(ellipse 70% 60% at 50% 50%, rgba(0,220,80,0.35) 0%, transparent 70%); }
.cd-cvr .cf-card[data-i="3"] .card-title-sm { color: rgba(80,220,120,0.7); }
.cd-cvr .cf-card[data-i="3"] .card-title-lg { color: #c8ffe0; }
.cd-cvr .cf-card[data-i="3"] .card-sub { color: rgba(80,220,120,0.5); }

.cd-cvr .cf-card[data-i="4"] .card-art { background: linear-gradient(148deg, #1a0010 0%, #3a0028 40%, #220018 100%); }
.cd-cvr .cf-card[data-i="4"] .card-art::before { background: radial-gradient(ellipse 70% 60% at 40% 35%, rgba(255,40,180,0.38) 0%, transparent 70%); }
.cd-cvr .cf-card[data-i="4"] .card-title-sm { color: rgba(255,140,200,0.7); }
.cd-cvr .cf-card[data-i="4"] .card-title-lg { color: #ffd8f0; }
.cd-cvr .cf-card[data-i="4"] .card-sub { color: rgba(255,140,200,0.5); }

.cd-cvr .cf-card[data-i="5"] .card-art { background: linear-gradient(148deg, #0a0a00 0%, #1e1e00 40%, #121200 100%); }
.cd-cvr .cf-card[data-i="5"] .card-art::before { background: radial-gradient(ellipse 70% 60% at 60% 30%, rgba(220,220,0,0.35) 0%, transparent 70%); }
.cd-cvr .cf-card[data-i="5"] .card-title-sm { color: rgba(220,220,80,0.7); }
.cd-cvr .cf-card[data-i="5"] .card-title-lg { color: #fffff0; }
.cd-cvr .cf-card[data-i="5"] .card-sub { color: rgba(220,220,80,0.5); }

.cd-cvr .cf-card[data-i="6"] .card-art { background: linear-gradient(148deg, #000e18 0%, #001c30 40%, #001020 100%); }
.cd-cvr .cf-card[data-i="6"] .card-art::before { background: radial-gradient(ellipse 70% 60% at 50% 60%, rgba(40,120,200,0.35) 0%, transparent 70%); }
.cd-cvr .cf-card[data-i="6"] .card-title-sm { color: rgba(100,180,255,0.7); }
.cd-cvr .cf-card[data-i="6"] .card-title-lg { color: #d8eeff; }
.cd-cvr .cf-card[data-i="6"] .card-sub { color: rgba(100,180,255,0.5); }

/* Decorative geo blobs */
.cd-cvr .geo { position: absolute; border-radius: 50%; opacity: 0.15; }
.cd-cvr .geo-1a { width: 140px; height: 140px; background: #a060ff; top: -30px; right: -30px; }
.cd-cvr .geo-1b { width: 70px; height: 70px; background: #6020ff; bottom: 40%; left: 20%; }
.cd-cvr .geo-2a { width: 180px; height: 180px; background: #00aaff; top: -50px; left: -30px; }
.cd-cvr .geo-2b { width: 55px; height: 55px; background: #00e8ff; bottom: 30%; right: 15%; }
.cd-cvr .geo-3a { width: 110px; height: 110px; background: #ff6600; top: 20px; right: 20px; }
.cd-cvr .geo-3b { width: 160px; height: 160px; background: #ff3300; bottom: -30px; left: -30px; }
.cd-cvr .geo-4a { width: 130px; height: 130px; background: #00cc44; top: -20px; left: 50%; margin-left: -65px; }
.cd-cvr .geo-4b { width: 80px; height: 80px; background: #00ff88; bottom: 20%; right: 10%; }
.cd-cvr .geo-5a { width: 140px; height: 140px; background: #ff00cc; top: 10px; right: -30px; }
.cd-cvr .geo-5b { width: 90px; height: 90px; background: #cc0088; bottom: 30%; left: 0%; }
.cd-cvr .geo-6a { width: 180px; height: 180px; background: #dddd00; top: -40px; left: -20px; }
.cd-cvr .geo-6b { width: 60px; height: 60px; background: #ffff40; bottom: 35%; right: 20%; }
.cd-cvr .geo-7a { width: 120px; height: 120px; background: #1c78cc; top: 30px; left: 30px; }
.cd-cvr .geo-7b { width: 100px; height: 100px; background: #3090e0; bottom: 20%; right: -10px; }

/* Nav */
.cd-cvr .nav-dots {
  display: flex;
  gap: 8px;
  margin-top: 24px;
  position: relative;
  z-index: 10;
}
.cd-cvr .dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: rgba(255,255,255,0.2);
  cursor: pointer;
  border: none;
  padding: 0;
  transition: all 0.3s;
}
.cd-cvr .dot.active {
  width: 22px;
  border-radius: 3px;
  background: rgba(255,255,255,0.7);
}

.cd-cvr .arrows {
  position: absolute;
  bottom: 28px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 16px;
  z-index: 20;
}
.cd-cvr .arr {
  width: 44px; height: 44px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.12);
  background: rgba(255,255,255,0.04);
  color: rgba(255,255,255,0.6);
  font-size: 18px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s;
  backdrop-filter: blur(8px);
  font-family: sans-serif;
}
.cd-cvr .arr:hover {
  background: rgba(255,255,255,0.1);
  border-color: rgba(255,255,255,0.25);
  color: #fff;
  transform: scale(1.08);
}
.cd-cvr .arr:active { transform: scale(0.96); }

@media (max-width: 720px) {
  .cd-cvr { height: 560px; }
  .cd-cvr .cf-card { width: 200px; height: 268px; margin-left: -100px; margin-top: -134px; }
}

@media (prefers-reduced-motion: reduce) {
  .cd-cvr .cf-card { transition: none !important; }
}
JS
(() => {
  const root = document.querySelector('.cd-cvr');
  if (!root) return;
  const stage = root.querySelector('[data-cd-cvr-stage]');
  const dotsEl = root.querySelector('[data-cd-cvr-dots]');
  const prevBtn = root.querySelector('[data-cd-cvr-prev]');
  const nextBtn = root.querySelector('[data-cd-cvr-next]');
  if (!stage || !dotsEl || !prevBtn || !nextBtn) return;

  const cards = Array.from(stage.querySelectorAll('.cf-card'));
  const N = cards.length;
  let current = Math.floor(N / 2);

  cards.forEach((_, i) => {
    const d = document.createElement('button');
    d.type = 'button';
    d.className = 'dot' + (i === current ? ' active' : '');
    d.setAttribute('aria-label', `Go to slide ${i + 1}`);
    d.addEventListener('click', () => goTo(i));
    dotsEl.appendChild(d);
  });

  function updatePositions() {
    const ANGLE_STEP = 42;
    const Z_OFFSET = 80;
    const SCALE_FALLOFF = 0.12;
    cards.forEach((card, i) => {
      const offset = i - current;
      const absOff = Math.abs(offset);
      const angle = offset * ANGLE_STEP;
      const z = -absOff * Z_OFFSET;
      const scale = Math.max(0.45, 1 - absOff * SCALE_FALLOFF);
      const opacity = absOff > 3 ? 0 : Math.max(0.25, 1 - absOff * 0.22);
      const brightness = Math.max(0.35, 1 - absOff * 0.2);
      card.style.transform = `rotateY(${angle}deg) translateZ(${z}px) scale(${scale})`;
      card.style.opacity = opacity;
      card.style.filter = `brightness(${brightness})`;
      card.style.zIndex = 10 - absOff;
      card.style.pointerEvents = absOff <= 1 ? 'auto' : 'none';
    });
    Array.from(dotsEl.children).forEach((d, i) => {
      d.className = 'dot' + (i === current ? ' active' : '');
    });
  }

  function goTo(n) {
    current = Math.max(0, Math.min(N - 1, n));
    updatePositions();
  }

  prevBtn.addEventListener('click', () => goTo(current - 1));
  nextBtn.addEventListener('click', () => goTo(current + 1));

  cards.forEach((card, i) => {
    card.addEventListener('click', () => { if (i !== current) goTo(i); });
  });

  // Keyboard — only when wrapper has focus or pointer is inside it
  let pointerInside = false;
  root.addEventListener('mouseenter', () => { pointerInside = true; });
  root.addEventListener('mouseleave', () => { pointerInside = false; });
  document.addEventListener('keydown', e => {
    if (!pointerInside) return;
    if (e.key === 'ArrowLeft')  { goTo(current - 1); e.preventDefault(); }
    if (e.key === 'ArrowRight') { goTo(current + 1); e.preventDefault(); }
  });

  // Drag scoped to wrapper
  let dragStart = null;
  root.addEventListener('mousedown', e => { dragStart = e.clientX; });
  root.addEventListener('mouseup', e => {
    if (dragStart === null) return;
    const dx = e.clientX - dragStart;
    if (Math.abs(dx) > 40) goTo(current + (dx < 0 ? 1 : -1));
    dragStart = null;
  });
  root.addEventListener('mouseleave', () => { dragStart = null; });
  root.addEventListener('touchstart', e => { dragStart = e.touches[0].clientX; }, { passive: true });
  root.addEventListener('touchend', e => {
    if (dragStart === null) return;
    const dx = e.changedTouches[0].clientX - dragStart;
    if (Math.abs(dx) > 40) goTo(current + (dx < 0 ? 1 : -1));
    dragStart = null;
  });

  updatePositions();
})();