Pure CSS Shapes & solids #loop #shape

Twisting puzzle cube in pure CSS

A 3×3×3 cube built as three layers that take turns twisting a quarter turn. Each layer is one flat box with the stickers painted on by gradients.

  • 69 lines of CSS
  • 7 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. 27 cubelets would be 162 faces. But a horizontal layer only ever turns as one piece, so each layer is one flat box (6 faces), and the 3 × 3 stickers are painted on with two repeating gradients.
  2. All three layers run the same 12-second timeline: a quarter turn at 0%, 25%, 50% and 75%, holding in between. Negative delays of -11s and -10s shift layers 2 and 3 one and two seconds later, so they take turns.
  3. After four quarter turns every layer is back at 360°, the same as 0°, so the loop is seamless and the cube solves itself each cycle.
  4. The black faces between layers use backface-visibility: hidden: the two touching faces point in opposite directions, so only one is ever drawn and they never flicker.

Key techniques

  • one box per layer, not 27 cubelets
  • gradient sticker grid
  • shared keyframes + negative delays take turns
  • backface-visibility on inner faces

Copy-paste code

HTML 7 lines

<div class="scene">
  <div class="rubik">
    <div class="layer"><i></i><i></i><i></i><i></i><b class="top"></b><b></b></div>
    <div class="layer"><i></i><i></i><i></i><i></i><b></b><b></b></div>
    <div class="layer"><i></i><i></i><i></i><i></i><b></b><b class="bottom"></b></div>
  </div>
</div>

CSS 69 lines

.scene {
  perspective: 800px;
}

.rubik {
  position: relative;
  width: 120px;
  height: 120px;
  transform-style: preserve-3d;
  transform: rotateX(-28deg) rotateY(-38deg);
}

/* one layer = one flat box, 120 x 40 x 120 */
.layer {
  position: absolute;
  left: 0;
  width: 120px;
  height: 40px;
  transform-style: preserve-3d;
  animation: twist 12s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

.layer:nth-child(1) { top: 0; }
.layer:nth-child(2) { top: 40px; animation-delay: -11s; } /* one second later */
.layer:nth-child(3) { top: 80px; animation-delay: -10s; } /* two seconds later */

/* stickers: black lines on the tile edges, 3 across and --row down */
.layer > * {
  position: absolute;
  left: 0;
  background-color: var(--c, #10121f);
  background-image:
    linear-gradient(90deg, #10121f 3px, transparent 3px calc(100% - 3px), #10121f calc(100% - 3px)),
    linear-gradient(#10121f 3px, transparent 3px calc(100% - 3px), #10121f calc(100% - 3px));
  background-size: 33.334% 100%, 100% var(--row, 100%);
  backface-visibility: hidden;
}

/* four sides: strips of three stickers */
.layer i { top: 0; width: 120px; height: 40px; }
.layer i:nth-child(1) { --c: #8b6cff; transform: translateZ(60px); }
.layer i:nth-child(2) { --c: #2ee6d6; transform: rotateY(90deg) translateZ(60px); }
.layer i:nth-child(3) { --c: #ff4d9d; transform: rotateY(180deg) translateZ(60px); }
.layer i:nth-child(4) { --c: #ffb547; transform: rotateY(-90deg) translateZ(60px); }

/* top and bottom of a layer: black inside the cube, stickers only on the outside */
.layer b {
  --row: 33.334%;
  top: -40px;
  width: 120px;
  height: 120px;
  transform: rotateX(90deg) translateZ(20px);
}

.layer b + b {
  transform: rotateX(-90deg) translateZ(20px);
}

.layer .top    { --c: #f2f3ff; }
.layer .bottom { --c: #4d8dff; }

/* a quarter turn in the first 6% of each quarter, then hold */
@keyframes twist {
  0%       { transform: rotateY(0deg); }
  6%, 25%  { transform: rotateY(90deg); }
  31%, 50% { transform: rotateY(180deg); }
  56%, 75% { transform: rotateY(270deg); }
  81%, 100% { transform: rotateY(360deg); }
}

Sass source 132 lines

@use 'sass:math';

$s: 90px; // whole cube
$l: math.div($s, 3); // one layer
$k: #10121f; // the black plastic between stickers
$line: 2.5px;

.d-rubik {
  position: relative;
  width: $s;
  height: $s;
  transform-style: preserve-3d;
  transform: rotateX(-28deg) rotateY(-38deg);

  // 27 cubelets would be 162 faces. A layer only ever turns as one piece, so each layer is ONE
  // flat box (6 faces) and the 3x3 stickers are painted on with gradients.
  &__layer {
    position: absolute;
    left: 0;
    width: $s;
    height: $l;
    transform-style: preserve-3d;
    // One shared animation. Every layer runs the same 12-slot timeline (a quarter turn at the
    // start of every third slot); negative delays put layer 2 one slot behind and layer 3 two
    // slots behind, so they take turns. After 12 slots each layer has done 360deg: a seamless loop.
    animation: d-rubik-twist 12s cubic-bezier(0.65, 0, 0.35, 1) infinite;

    &:nth-child(1) {
      top: 0;
    }

    &:nth-child(2) {
      top: $l;
      animation-delay: -11s;
    }

    &:nth-child(3) {
      top: $l * 2;
      animation-delay: -10s;
    }
  }

  i,
  b {
    position: absolute;
    left: 0;
    background-color: var(--c, #{$k});
    // two line patterns: vertical gaps repeat three times across, horizontal gaps repeat --rows times
    background-image:
      linear-gradient(90deg, $k $line, transparent $line calc(100% - #{$line}), $k calc(100% - #{$line})),
      linear-gradient($k $line, transparent $line calc(100% - #{$line}), $k calc(100% - #{$line}));
    background-size:
      33.334% 100%,
      100% var(--row, 100%);
    backface-visibility: hidden;
  }

  // the four sides of a layer: a strip of three stickers
  i {
    top: 0;
    width: $s;
    height: $l;

    &:nth-child(1) {
      --c: var(--accent);
      transform: translateZ($s * 0.5);
    }

    &:nth-child(2) {
      --c: var(--accent-2);
      transform: rotateY(90deg) translateZ($s * 0.5);
    }

    &:nth-child(3) {
      --c: var(--hot);
      transform: rotateY(180deg) translateZ($s * 0.5);
    }

    &:nth-child(4) {
      --c: var(--warm);
      transform: rotateY(-90deg) translateZ($s * 0.5);
    }
  }

  // top and bottom of a layer: full squares, centred on the layer, pushed out by half its height.
  // Inside the cube they stay plain black; only the outer two carry stickers (class --cap).
  b {
    --row: 33.334%;
    top: ($l - $s) * 0.5;
    width: $s;
    height: $s;
    transform: rotateX(90deg) translateZ($l * 0.5);

    & + b {
      transform: rotateX(-90deg) translateZ($l * 0.5);
    }
  }

  &__cap {
    --c: #f2f3ff; // top of the cube
  }

  &__layer:nth-child(3) &__cap {
    --c: #4d8dff; // bottom of the cube
  }
}

@keyframes d-rubik-twist {
  0% {
    transform: rotateY(0deg);
  }

  6%,
  25% {
    transform: rotateY(90deg);
  }

  31%,
  50% {
    transform: rotateY(180deg);
  }

  56%,
  75% {
    transform: rotateY(270deg);
  }

  81%,
  100% {
    transform: rotateY(360deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.