Pure CSS Shapes & solids #loop #shape

Glass tetrahedron in pure CSS

Four equilateral triangles: three hinged on the edges of the fourth and leaned in by 19.47°, the angle at which their tips meet. A glowing core turns back against the tumble to stay round.

  • 77 lines of CSS
  • 9 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. A regular tetrahedron is four equilateral triangles. Each face is a box a wide and a × √3 / 2 tall, cut with clip-path: polygon(50% 0, 0 100%, 100% 100%).
  2. Stand three of them on the edges of the floor triangle, like carousel panels: rotateY(i × 120deg) translateZ(r), where r = a / (2√3) is the distance from the floor's centre to the middle of an edge.
  3. Hinge each on its bottom edge and lean it in. The faces of a tetrahedron meet the floor at atan(2√2) ≈ 70.53°, so an upright face leans the other 19.47°, and the three tips meet exactly above the centre. The floor is the same triangle leaned all the way, 90°, so its tip lands on the back corner.
  4. Tumble it round its centroid, a quarter of the height up from the floor (transform-origin), not round the box centre, or it wobbles as it turns.
  5. The core undoes the tumble: the same three turns backwards and in reverse order, on the same timing. So it always faces you and stays a round glow.

Key techniques

  • clip-path triangles
  • lean = 90° − atan(2√2)
  • transform-origin on the centroid
  • inverse keyframes billboard

Copy-paste code

HTML 9 lines

<div class="scene">
  <div class="tetra">
    <i style="--i:0"></i>
    <i style="--i:1"></i>
    <i style="--i:2"></i>
    <b></b>
    <u></u>
  </div>
</div>

CSS 77 lines

.scene {
  perspective: 800px;
}

.tetra {
  --a: 140px;    /* edge length */
  --r: 40.41px;  /* floor inradius = a / (2√3) */
  position: relative;
  width: var(--a);
  height: 121.24px;              /* face height = a × √3 / 2 */
  transform-style: preserve-3d;
  transform-origin: 50% 92.66px; /* the centroid: it stands a·√(2/3) = 114.31px tall, a quarter of that up */
  animation: tumble 16s linear infinite;
}

.tetra i,
.tetra b {
  --c: 46 230 214;
  --alpha: 0.34;
  --edge: rgb(var(--c) / 0.95);
  position: absolute;
  inset: 0;
  transform-origin: 50% 100%; /* the hinge: the bottom edge, on the floor */
  clip-path: polygon(50% 0, 0 100%, 100% 100%);
  /* borders are clipped away, so the edges are painted: a "to corner" gradient's 50% line is its box diagonal */
  background:
    linear-gradient(to top left, transparent calc(50% - 1.5px), var(--edge) calc(50% - 1.5px) 50%, transparent 50%) left / 50% 100% no-repeat,
    linear-gradient(to top right, transparent calc(50% - 1.5px), var(--edge) calc(50% - 1.5px) 50%, transparent 50%) right / 50% 100% no-repeat,
    linear-gradient(var(--edge), var(--edge)) bottom / 100% 1.5px no-repeat,
    linear-gradient(90deg, transparent 42%, rgb(255 255 255 / 0.16) 50%, transparent 58%),
    linear-gradient(to top, rgb(var(--c) / var(--alpha)), rgb(var(--c) / 0.08));
}

/* the sides: 120deg apart, out to an edge, leaned in by 90deg - atan(2√2) = 19.47deg */
.tetra i {
  transform: rotateY(calc(var(--i) * 120deg)) translateZ(var(--r)) rotateX(19.47deg);
}

.tetra i:nth-child(2) { --c: 139 108 255; --alpha: 0.44; }
.tetra i:nth-child(3) { --c: 255 77 157;  --alpha: 0.3; }

/* the floor: the same triangle on the front edge, folded flat */
.tetra b {
  --c: 139 108 255;
  --alpha: 0.26;
  transform: translateZ(var(--r)) rotateX(90deg);
}

/* the core sits on the centroid */
.tetra u {
  position: absolute;
  top: 92.66px;
  left: 50%;
  width: 40px;
  height: 40px;
  margin: -20px 0 0 -20px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0 8%, #ffcb7e 22%, rgb(255 181 71 / 0.55) 42%, transparent 70%);
  animation: face-you 16s linear infinite, pulse 2.4s ease-in-out infinite alternate;
}

/* -24deg + 360deg = 336deg: the end pose equals the start pose */
@keyframes tumble {
  from { transform: rotateX(-24deg) rotateY(0deg) rotateZ(0deg); }
  to   { transform: rotateX(336deg) rotateY(360deg) rotateZ(360deg); }
}

/* the exact inverse of the tumble: the same turns backwards, in reverse order */
@keyframes face-you {
  from { transform: rotateZ(0deg) rotateY(0deg) rotateX(24deg); }
  to   { transform: rotateZ(-360deg) rotateY(-360deg) rotateX(-336deg); }
}

@keyframes pulse {
  from { opacity: 0.7; scale: 0.85; }
  to   { opacity: 1; scale: 1.1; }
}

Sass source 119 lines

@use 'sass:math';

$a: 120px; // edge length
$t: $a * math.div(math.sqrt(3), 2); // height of one equilateral face (103.92px)
$r: math.div($a, 2 * math.sqrt(3)); // base inradius: centre of the floor to the middle of an edge (34.64px)
// A face meets the floor at atan(2√2) = 70.53deg. Faces start upright, so they lean the rest.
$lean: 90deg - math.atan(2 * math.sqrt(2)); // 19.47deg
$H: $a * math.sqrt(math.div(2, 3)); // how tall it stands (97.98px)
$mid: $t - $H * 0.25; // the centroid, a quarter of the way up, measured from the box top
$edge: color-mix(in srgb, var(--c) 85%, #fff);
$spin: 16s;

// A glass tetrahedron with a glowing core. Every face is the same triangle hinged on one edge of
// the floor triangle: the three sides lean in by 19.47deg, the floor "leans" all the way, 90deg.
.d-tetra {
  position: relative;
  width: $a;
  height: $t;
  transform-style: preserve-3d;
  transform-origin: 50% $mid; // tumble round the centroid, not the box centre
  animation: d-tetra-tumble $spin linear infinite;

  i,
  b {
    --c: var(--accent-2);
    --a: 34%;
    position: absolute;
    inset: 0;
    transform-origin: 50% 100%; // the hinge: the bottom edge, on the floor
    clip-path: polygon(50% 0, 0 100%, 100% 100%);
    // clip-path cuts borders away, so the edges are painted: a "to corner" gradient's 50% line
    // runs exactly along the diagonal of its box, and each half of the face gets one
    background:
      linear-gradient(to top left, transparent calc(50% - 1.5px), $edge calc(50% - 1.5px), $edge 50%, transparent 50%) left /
        50% 100% no-repeat,
      linear-gradient(to top right, transparent calc(50% - 1.5px), $edge calc(50% - 1.5px), $edge 50%, transparent 50%)
        right / 50% 100% no-repeat,
      linear-gradient($edge, $edge) bottom / 100% 1.5px no-repeat,
      linear-gradient(90deg, transparent 42%, rgb(255 255 255 / 0.16) 50%, transparent 58%),
      linear-gradient(to top, color-mix(in srgb, var(--c) var(--a), transparent), color-mix(in srgb, var(--c) 8%, transparent));
  }

  // the three sides: turned 120deg apart, stepped out to an edge, leaned in
  i {
    transform: rotateY(calc(var(--i) * 120deg)) translateZ($r) rotateX($lean);

    &:nth-child(2) {
      --c: var(--accent);
      --a: 44%;
    }

    &:nth-child(3) {
      --c: var(--hot);
      --a: 30%;
    }
  }

  // the floor: the same triangle on the front edge, folded flat (its tip points to the back corner)
  b {
    --c: var(--accent);
    --a: 26%;
    transform: translateZ($r) rotateX(90deg);
  }

  // the core sits on the centroid and turns back against the tumble, so it always faces you
  u {
    position: absolute;
    top: $mid;
    left: 50%;
    width: 34px;
    height: 34px;
    margin: -17px 0 0 -17px;
    border-radius: 50%;
    background: radial-gradient(
      circle,
      #fff 0 8%,
      color-mix(in srgb, var(--warm) 70%, #fff) 22%,
      color-mix(in srgb, var(--warm) 55%, transparent) 42%,
      transparent 70%
    );
    animation:
      d-tetra-face $spin linear infinite,
      d-tetra-pulse 2.4s ease-in-out infinite alternate;
  }
}

// -24deg + 360deg = 336deg: the end pose equals the start pose
@keyframes d-tetra-tumble {
  from {
    transform: rotateX(-24deg) rotateY(0deg) rotateZ(0deg);
  }

  to {
    transform: rotateX(336deg) rotateY(360deg) rotateZ(360deg);
  }
}

// the exact inverse: the same three turns backwards, in reverse order
@keyframes d-tetra-face {
  from {
    transform: rotateZ(0deg) rotateY(0deg) rotateX(24deg);
  }

  to {
    transform: rotateZ(-360deg) rotateY(-360deg) rotateX(-336deg);
  }
}

@keyframes d-tetra-pulse {
  from {
    opacity: 0.7;
    scale: 0.85;
  }

  to {
    opacity: 1;
    scale: 1.1;
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.