Pure CSS Data & tools #loop #chart #data #donut

3D donut chart in pure CSS

A conic-gradient disc repeated in twelve translateZ layers. Lower layers are darkened, which reads as a solid wall, and a radial mask on each layer cuts the hole.

  • 74 lines of CSS
  • 26 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. One disc: a conic-gradient with hard stops at the running totals (40%, 65%, 85%). Repeat it in twelve layers, each translateZ(var(--i) * 1.5px) higher.
  2. Tilt the stack. The rims of the lower layers peek out under the top one and together read as a solid side wall. A dark overlay whose alpha grows as --i shrinks shades that wall.
  3. The hole is a radial-gradient mask on each layer. A mask on the spinning parent would flatten all twelve layers into one picture.
  4. Spin a wrapper with rotateZ inside the tilt, so every slice passes the front in turn.

Key techniques

  • conic-gradient segments
  • stacked translateZ layers
  • darkening by --i
  • mask on the leaf layers only

Copy-paste code

HTML 26 lines

<div class="scene">
  <div class="donut">
    <div class="tilt">
      <div class="spin">
        <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>
      </div>
    </div>
    <ul class="legend">
      <li style="--c:#8b6cff">Design 40%</li>
      <li style="--c:#2ee6d6">Build 25%</li>
      <li style="--c:#ff4d9d">Test 20%</li>
      <li style="--c:#ffb547">Ship 15%</li>
    </ul>
  </div>
</div>

CSS 74 lines

.scene {
  perspective: 800px;
}

.donut {
  display: grid;
  justify-items: center;
  gap: 10px;
}

.tilt {
  position: relative;
  width: 136px;
  height: 136px;
  margin: -16px 0 -30px; /* the tilted disc is much flatter than its box */
  transform-style: preserve-3d;
  transform: rotateX(60deg);
}

.spin {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  animation: spin 18s linear infinite;
}

.spin i {
  --shade: rgb(0 0 0 / calc((11 - var(--i)) * 0.05)); /* 0.55 at the bottom, 0 on top */
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    linear-gradient(var(--shade), var(--shade)),
    conic-gradient(#8b6cff 0 40%, #2ee6d6 0 65%, #ff4d9d 0 85%, #ffb547 0);
  /* the hole: on each layer, never on a parent */
  mask: radial-gradient(closest-side, transparent 49.5%, #000 50%);
  transform: translateZ(calc(var(--i) * 1.5px));
}

/* top layer: a sheen and thin white slice edges */
.spin i:last-child {
  background:
    radial-gradient(circle at 30% 25%, rgb(255 255 255 / 0.35), transparent 55%),
    conic-gradient(#8b6cff 0 39.6%, #fff 0 40%, #2ee6d6 0 64.6%, #fff 0 65%, #ff4d9d 0 84.6%, #fff 0 85%, #ffb547 0 99.6%, #fff 0);
}

.legend {
  display: grid;
  grid-template-columns: auto auto;
  gap: 3px 16px;
  margin: 0;
  padding: 0;
  color: #949bc0;
  font: 600 11px system-ui;
  list-style: none;
}

.legend li {
  display: flex;
  align-items: center;
  gap: 6px;
}

.legend li::before {
  content: '';
  width: 9px;
  height: 9px;
  border-radius: 3px;
  background: var(--c);
}

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

Sass source 103 lines

// The same conic-gradient disc, twelve times, each layer 1.5px higher (--i = 0 … 11).
// Seen at an angle, the edges of the lower layers peek out below the top one and read as a
// solid wall. Lower layers get a darker overlay, so the wall is shaded like a real side.
// The hole is a radial mask on each LAYER (a leaf): a mask on the spinning parent would
// flatten the whole stack into one plane.
$size: 136px;
$gap: 1.5px;
$hole: 50%; // inner radius as a share of the outer one

.d-pie {
  display: grid;
  justify-items: center;
  gap: 10px;
  transform-style: preserve-3d;

  &__tilt {
    position: relative;
    width: $size;
    height: $size;
    margin: -16px 0 -30px; // the tilted disc is much flatter than its layout box
    transform-style: preserve-3d;
    transform: rotateX(60deg);

    // a soft shadow on the "table", under the lowest layer
    &::before {
      content: '';
      position: absolute;
      inset: -10%;
      border-radius: 50%;
      background: radial-gradient(closest-side, rgb(0 0 0 / 0.35), transparent);
      transform: translateZ(-2px);
    }
  }

  &__spin {
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
    animation: d-pie-spin 18s linear infinite;

    i {
      position: absolute;
      inset: 0;
      border-radius: 50%;
      background:
        // darkening: 0.55 on the bottom layer, 0 on the top one
        linear-gradient(rgb(0 0 0 / calc((11 - var(--i)) * 0.05)), rgb(0 0 0 / calc((11 - var(--i)) * 0.05))),
        conic-gradient(var(--accent) 0 40%, var(--accent-2) 0 65%, var(--hot) 0 85%, var(--warm) 0);
      mask: radial-gradient(closest-side, transparent calc(#{$hole} - 0.5%), #000 $hole);
      transform: translateZ(calc(var(--i) * #{$gap}));
    }

    // the top layer gets a glassy sheen and bright segment edges
    i:last-child {
      background:
        radial-gradient(circle at 30% 25%, rgb(255 255 255 / 0.35), transparent 55%),
        conic-gradient(
          var(--accent) 0 39.6%, #fff 0 40%,
          var(--accent-2) 0 64.6%, #fff 0 65%,
          var(--hot) 0 84.6%, #fff 0 85%,
          var(--warm) 0 99.6%, #fff 0
        );
    }
  }

  &__legend {
    display: grid;
    grid-template-columns: auto auto;
    gap: 3px 16px;
    margin: 0;
    padding: 0;
    color: var(--muted);
    font-size: 11px;
    font-weight: 600;
    list-style: none;

    li {
      display: flex;
      align-items: center;
      gap: 6px;

      &::before {
        content: '';
        width: 9px;
        height: 9px;
        border-radius: 3px;
        background: var(--c);
      }
    }

    @each $n, $c in (1: var(--accent), 2: var(--accent-2), 3: var(--hot), 4: var(--warm)) {
      li:nth-child(#{$n}) {
        --c: #{$c};
      }
    }
  }
}

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