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.

  • 87 lines of CSS
  • 9 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

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 loop starts 14.5s into the turn (a negative delay), on a pose seen square and centred, because the first frame is what a paused card shows.
  6. 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.
  7. Every length is a multiple of one base unit, --u, tied to the canvas, so the tetrahedron is the same share of a gallery card, the editor and a recording canvas. The numbers above are those units.

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 87 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the tetrahedron is the same share of a
     card, the editor, a full screen and a recording canvas */
  --u: 0.4vmin;
  perspective: calc(800 * var(--u));
}

.tetra {
  --a: calc(140 * var(--u));    /* edge length */
  --r: calc(40.41 * var(--u));  /* floor inradius = a / (2√3) */
  position: relative;
  width: var(--a);
  height: calc(121.24 * var(--u));              /* face height = a × √3 / 2 */
  transform-style: preserve-3d;
  transform-origin: 50% calc(92.66 * var(--u)); /* the centroid: it stands a·√(2/3) = 114.31 units tall, a quarter of that up */
  /* it turns round the centroid, 32 units below the middle of its box. The eye reads the middle
     of what it sees, and over a turn that sits on average 5 units above the centroid (the apex
     reaches further than the floor), so the box is lifted by 27 units, not 32, and the tumble as
     seen is what sits in the middle of the canvas */
  translate: 0 calc(-27 * var(--u));
  /* the loop starts 14.5s into the turn: a pose seen square, centred on the canvas, which is what
     a paused card shows. The core starts at the same moment, so it still undoes the tumble */
  animation: tumble 16s linear -14.5s 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.5 * var(--u)), var(--edge) calc(50% - 1.5 * var(--u)) 50%, transparent 50%) left / 50% 100% no-repeat,
    linear-gradient(to top right, transparent calc(50% - 1.5 * var(--u)), var(--edge) calc(50% - 1.5 * var(--u)) 50%, transparent 50%) right / 50% 100% no-repeat,
    linear-gradient(var(--edge), var(--edge)) bottom / 100% calc(1.5 * var(--u)) 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: calc(92.66 * var(--u));
  left: 50%;
  width: calc(40 * var(--u));
  height: calc(40 * var(--u));
  margin: calc(-20 * var(--u)) 0 0 calc(-20 * var(--u));
  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 -14.5s 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; }
}
Standalone snippet · plain CSS, no build step, no dependencies.