Pure CSS Shapes & solids #hover #shape

Exploding cube in pure CSS

Hover to blow the faces apart. One custom property moves all six.

  • 55 lines of CSS
  • 6 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Every face uses translateZ(var(--d)) instead of a fixed half-side distance.
  2. Hovering the wrapper changes --d — one declaration moves all six faces.
  3. Because each face has a transition on transform, the change animates even though only a custom property was touched.
  4. Hover is detected on a larger, non-rotating wrapper so the target does not spin out from under the pointer.
  5. Every length is a multiple of one base unit, --u, tied to the canvas: the side is 110 units, so a closed cube would have --d at 55; it rests at 64, the faces standing a little apart so even a paused cube shows six loose panels, and hover pushes it to 108. The cube, and how far it flies apart, are the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • translateZ(var(--d))
  • transition on a var()-driven transform
  • spin continues underneath

Copy-paste code

HTML 6 lines

<div class="scene" tabindex="0" role="img" aria-label="Cube whose faces fly apart on hover or focus">
  <div class="cube">
    <div></div><div></div><div></div>
    <div></div><div></div><div></div>
  </div>
</div>

CSS 55 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the cube is the same share of a
     card, the editor, a full screen and a recording canvas */
  --u: 0.28vmin;
  --s: calc(110 * var(--u));
  /* distance of each face from the centre: at rest a little more than half the side, so the
     faces stand just apart and even a paused cube shows it is six loose panels */
  --d: calc(64 * var(--u));
  display: grid;
  place-items: center;
  /* a hit area wider than the cube, so the pointer does not fall off it between the faces */
  width: calc(300 * var(--u));
  height: calc(300 * var(--u));
  perspective: calc(800 * var(--u));
  cursor: pointer;
}

.scene:hover,
.scene:focus {
  --d: calc(108 * var(--u));
}

.cube {
  position: relative;
  width: var(--s);
  height: var(--s);
  transform-style: preserve-3d;
  animation: spin 10s linear infinite;
}

.cube > * {
  position: absolute;
  inset: 0;
  border-radius: calc(6 * var(--u));
  background: rgb(255 77 157 / 0.35);
  border: calc(1 * var(--u)) solid rgb(255 77 157 / 0.85);
  transition: transform 0.6s cubic-bezier(0.3, 1.5, 0.5, 1);
}

.cube > :nth-child(1) { transform: rotateY(0deg)   translateZ(var(--d)); }
.cube > :nth-child(2) { transform: rotateY(90deg)  translateZ(var(--d)); }
.cube > :nth-child(3) { transform: rotateY(180deg) translateZ(var(--d)); }
.cube > :nth-child(4) { transform: rotateY(-90deg) translateZ(var(--d)); }
.cube > :nth-child(5) { transform: rotateX(90deg)  translateZ(var(--d)); }
.cube > :nth-child(6) { transform: rotateX(-90deg) translateZ(var(--d)); }

/* The top and bottom are always seen at a slant, squeezed flat, and their edges sit on the
   same spot as the side faces' edges: two lines there step over each other and read as dashes.
   So only the sides draw the edge line. */
.cube > :nth-child(n + 5) { border-color: transparent; }

@keyframes spin {
  from { transform: rotateX(-24deg) rotateY(-35deg); }
  to   { transform: rotateX(-24deg) rotateY(325deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.