Pure CSS Shapes & solids #loop #shape #sass-loop

Cone in pure CSS

Sixteen thin triangles on a circle, each leaning in by atan(radius / height) so all the tips meet on the axis, over a flat disc base.

  • 59 lines of CSS
  • 23 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. Place 16 thin triangles on a circle like carousel panels: rotateY(i × 22.5deg) translateZ(r). Each is 2r × tan(180° / 16) wide, so their bottom edges close the ring.
  2. Hinge them on the bottom edge and lean them back by atan(r / h). That is exactly the angle at which every tip lands on the axis, h above the base.
  3. The triangle element must be as tall as the slant, √(h² + r²), not h, because leaning shortens it.
  4. One element cannot run two transform animations, so the rocking lives on a wrapper and the spin on its child.

Key techniques

  • triangle width = 2r × tan(180° / n)
  • lean = atan(r / h)
  • rocking wrapper + spinning child
  • Sass @for

Copy-paste code

HTML 23 lines

<div class="scene">
  <div class="cone">
    <div class="cone-body">
      <i style="--i:0"></i>
      <i style="--i:1"></i>
      <i style="--i:2"></i>
      <i style="--i:3"></i>
      <i style="--i:4"></i>
      <i style="--i:5"></i>
      <i style="--i:6"></i>
      <i style="--i:7"></i>
      <i style="--i:8"></i>
      <i style="--i:9"></i>
      <i style="--i:10"></i>
      <i style="--i:11"></i>
      <i style="--i:12"></i>
      <i style="--i:13"></i>
      <i style="--i:14"></i>
      <i style="--i:15"></i>
      <b></b>
    </div>
  </div>
</div>

CSS 59 lines

.scene {
  perspective: 800px;
}

/* wrapper: rocks back and forth */
.cone {
  transform-style: preserve-3d;
  animation: rock 7s ease-in-out infinite alternate;
}

/* child: spins */
.cone-body {
  position: relative;
  width: 19.89px; /* triangle base = 2 × 50px × tan(180deg / 16) */
  height: 110px;  /* cone height */
  transform-style: preserve-3d;
  animation: spin 10s linear infinite;
}

.cone-body i {
  --hue: calc(293 + 40 * cos(var(--i) * 22.5deg)); /* pink ... violet ... pink */
  --edge: hsl(var(--hue) 90% 68% / 0.8);
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 120.83px; /* slant = √(110² + 50²) */
  transform-origin: 50% 100%;
  /* stand on the base circle, then lean in by atan(50 / 110) */
  transform: rotateY(calc(var(--i) * 22.5deg)) translateZ(50px) rotateX(24.44deg);
  clip-path: polygon(50% 0, 0 100%, 100% 100%);
  background:
    linear-gradient(to top left, transparent calc(50% - 1px), var(--edge) calc(50% - 1px) 50%, transparent 50%) left / 50% 100% no-repeat,
    linear-gradient(to top right, transparent calc(50% - 1px), var(--edge) calc(50% - 1px) 50%, transparent 50%) right / 50% 100% no-repeat,
    linear-gradient(to top, hsl(var(--hue) 90% 68% / 0.45), hsl(var(--hue) 90% 68% / 0.14));
}

/* base disc through the triangles' corners: 2 × 50px / cos(180deg / 16) */
.cone-body b {
  position: absolute;
  left: calc(50% - 50.98px);
  top: calc(100% - 50.98px);
  width: 101.96px;
  height: 101.96px;
  border-radius: 50%;
  background: rgb(46 230 214 / 0.26);
  border: 1px solid rgb(46 230 214 / 0.75);
  box-shadow: inset 0 0 24px rgb(46 230 214 / 0.3);
  transform: rotateX(90deg);
}

@keyframes rock {
  from { transform: rotateX(-30deg) rotateZ(-6deg); }
  to   { transform: rotateX(24deg) rotateZ(6deg); }
}

@keyframes spin {
  to { transform: rotateY(360deg); }
}

Sass source 81 lines

@use 'sass:math';
@use '../mixins' as *;

$n: 16;
$R: 50px; // base radius (to the middle of a triangle's bottom edge)
$H: 110px;
$w: 2 * $R * math.tan(math.div(180deg, $n)); // bottom edge of one triangle
$L: math.hypot($H, $R); // slant length = height of the triangle element
$lean: math.atan(math.div($R, $H)); // how far each triangle leans in from upright
$disc: math.div($R, math.cos(math.div(180deg, $n))) * 2; // circle through the triangles' corners
$edge: color-mix(in srgb, var(--c) 80%, transparent);

// Two animations cannot share one transform, so the rocking and the spinning live on two elements.
.d-cone {
  transform-style: preserve-3d;
  animation: d-cone-rock 7s ease-in-out infinite alternate;

  &__body {
    position: relative;
    width: $w;
    height: $H;
    transform-style: preserve-3d;
    animation: d-cone-spin 10s linear infinite;
  }

  // Triangles stand upright on the base circle (like carousel panels), hinged on their bottom
  // edge, then lean back by atan(R / H): exactly enough for all the tips to meet on the axis.
  i {
    --c: color-mix(in srgb, var(--hot) var(--mix), var(--accent));
    position: absolute;
    left: 0;
    bottom: 0;
    width: $w;
    height: $L;
    transform-origin: 50% 100%;
    transform: rotateY(calc(var(--i) * #{math.div(360deg, $n)})) translateZ($R) rotateX($lean);
    clip-path: polygon(50% 0, 0 100%, 100% 100%);
    // slanted edges are drawn with "to corner" gradients: their 50% line is the box diagonal
    background:
      linear-gradient(to top left, transparent calc(50% - 1px), $edge calc(50% - 1px), $edge 50%, transparent 50%) left / 50%
        100% no-repeat,
      linear-gradient(to top right, transparent calc(50% - 1px), $edge calc(50% - 1px), $edge 50%, transparent 50%) right /
        50% 100% no-repeat,
      linear-gradient(to top, color-mix(in srgb, var(--c) 45%, transparent), color-mix(in srgb, var(--c) 14%, transparent));
  }

  @for $i from 0 to $n {
    i:nth-child(#{$i + 1}) {
      --i: #{$i};
      --mix: #{math.round(math.div(math.abs($i - $n * 0.5), $n * 0.5) * 100) * 1%};
    }
  }

  // base disc, laid flat at the bottom
  b {
    position: absolute;
    left: calc(50% - #{$disc * 0.5});
    top: calc(100% - #{$disc * 0.5});
    width: $disc;
    height: $disc;
    border-radius: 50%;
    transform: rotateX(90deg);
    @include face(var(--accent-2), 26%);
  }
}

@keyframes d-cone-rock {
  from {
    transform: rotateX(-30deg) rotateZ(-6deg);
  }

  to {
    transform: rotateX(24deg) rotateZ(6deg);
  }
}

@keyframes d-cone-spin {
  to {
    transform: rotateY(360deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.