CSS + JS Cards & galleries #controls #shape

Box slideshow in CSS + JavaScript

Four slides on the sides of a box. JS counts steps; CSS turns the box by 90° per step.

  • 67 lines of CSS
  • 18 lines of HTML
  • 9 lines of JS
  • No dependencies
  • MIT licensed
GitHub

How it works

  1. Four slides are the four side faces of a box: rotateY(n × 90deg) translateZ(width / 2).
  2. The box is pulled back by translateZ(-width / 2), so whichever face is in front sits at z = 0 and renders at its true size.
  3. JS keeps a plain counter that never wraps. CSS multiplies it: rotateY(calc(var(--step) * -90deg)).
  4. Because the angle keeps growing instead of resetting, "next" always turns the same way — no rewind from slide 4 to slide 1.

Key techniques

  • rotateY(calc(var(--step) * -90deg))
  • unbounded counter = endless rotation
  • translateZ(-w/2) keeps the front at z = 0

Copy-paste code

HTML 18 lines

<div class="slider">
  <div class="scene">
    <div class="box">
      <i style="--hue:250">Design</i>
      <i style="--hue:290">Build</i>
      <i style="--hue:330">Ship</i>
      <i style="--hue:10">Repeat</i>
    </div>
  </div>
  <nav>
    <button type="button" data-dir="-1" aria-label="Previous">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
    </button>
    <button type="button" data-dir="1" aria-label="Next">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>
    </button>
  </nav>
</div>

CSS 67 lines

.slider {
  display: grid;
  justify-items: center;
  gap: 50px;
}

.scene {
  perspective: 800px;
}

.box {
  --w: 280px;
  position: relative;
  width: var(--w);
  height: 170px;
  transform-style: preserve-3d;
  transform: translateZ(calc(var(--w) / -2)) rotateY(calc(var(--step, 0) * -90deg));
  transition: transform 0.7s cubic-bezier(0.3, 1.25, 0.5, 1);
}

.box i {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border-radius: 8px;
  color: #fff;
  font: 900 2rem system-ui;
  background: linear-gradient(135deg, hsl(var(--hue) 85% 62%), hsl(var(--hue) 75% 38%));
  backface-visibility: hidden;
  transform-style: preserve-3d;
}

/* plugs the see-through notches where the rounded corners of two slides meet */
.box i::before {
  content: '';
  position: absolute;
  inset: 6px;
  background: hsl(var(--hue) 75% 34%);
  transform: translateZ(-6px);
}

.box i:nth-child(1) { transform: rotateY(0deg)   translateZ(calc(var(--w) / 2)); }
.box i:nth-child(2) { transform: rotateY(90deg)  translateZ(calc(var(--w) / 2)); }
.box i:nth-child(3) { transform: rotateY(180deg) translateZ(calc(var(--w) / 2)); }
.box i:nth-child(4) { transform: rotateY(270deg) translateZ(calc(var(--w) / 2)); }

nav { display: flex; gap: 8px; }

nav button {
  width: 44px;
  height: 36px;
  border: 1px solid #5a6188;
  border-radius: 8px;
  display: grid;
  place-items: center;
  padding: 0;
  background: #161a2e;
  color: #fff;
  cursor: pointer;
}

/* SVG arrows, not text like ‹ ›: a glyph sits on the font's baseline, so it never centres */
nav svg {
  width: 18px;
  height: 18px;
}

JS 9 lines

const box = document.querySelector('.box');
let step = 0;   // never wraps, so the box keeps turning the same way

document.querySelector('nav').addEventListener('click', (e) => {
  const btn = e.target.closest('[data-dir]');
  if (!btn) return;
  step += Number(btn.dataset.dir);
  box.style.setProperty('--step', step);
});

Sass source 70 lines

@use '../mixins' as *;

$w: 190px;

.d-boxslider {
  display: grid;
  grid-template-rows: 1fr auto;
  justify-items: center;
  width: 100%;
  height: 100%;
  padding: 10px;

  &__view {
    display: grid;
    place-items: center;
    perspective: 700px;
  }

  &__box {
    position: relative;
    width: $w;
    height: 120px;
    transform-style: preserve-3d;
    // --step is an ever-growing (or shrinking) integer written by JS.
    transform: translateZ($w * -0.5) rotateY(calc(var(--step, 0) * -90deg));
    transition: transform 0.7s cubic-bezier(0.3, 1.25, 0.5, 1);

    i {
      position: absolute;
      inset: 0;
      display: grid;
      place-items: center;
      border-radius: 6px;
      background: linear-gradient(135deg, hsl(var(--hue) 85% 62%), hsl(var(--hue) 75% 38%));
      color: #fff;
      font-size: 26px;
      font-style: normal;
      font-weight: 900;
      backface-visibility: hidden;
      @include solid-core(5px, hsl(var(--hue) 75% 34%));

      @for $i from 1 through 4 {
        &:nth-child(#{$i}) {
          transform: rotateY(($i - 1) * 90deg) translateZ($w * 0.5);
        }
      }
    }
  }

  &__nav {
    display: flex;
    gap: 8px;

    button {
      width: 34px;
      height: 28px;
      border: 1px solid var(--border-strong);
      border-radius: 8px;
      background: var(--surface);
      display: grid;
      place-items: center;
      padding: 0;
      cursor: pointer;

      &:hover {
        border-color: var(--warm);
      }
    }
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.