HTML 29 lines
<div class="band">
<div class="view">
<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>
</div>
<div class="controls">
<p class="caption">Sprint time by stage, %</p>
<ul class="row 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 99 lines
/* the model box and the legend stand in one stack, so the legend is the same distance under
the donut in every model that has a control zone */
.band {
display: grid;
justify-items: center;
gap: 4vmin;
}
/* the model box: one base unit, and every length of the donut is a multiple of it */
.view {
--u: 0.44vmin;
display: grid;
place-items: center;
height: 50vmin;
perspective: calc(800 * var(--u));
}
.tilt {
position: relative;
width: calc(136 * var(--u));
height: calc(136 * var(--u));
margin: calc(-60 * var(--u)) 0; /* 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.5 * var(--u)));
}
/* top layer: a sheen and thin white slice edges. The edges fade in and out over a hair
instead of hard stops: a hard hairline wedge on the tilted disc steps and reads as a dashed line. */
.spin i:last-child {
background:
radial-gradient(circle at 30% 25%, rgb(255 255 255 / 0.35), transparent 55%),
conic-gradient(#fff 0 0.1%, #8b6cff 0.6% 39.2%, #fff 39.7% 40.1%, #2ee6d6 40.6% 64.2%, #fff 64.7% 65.1%, #ff4d9d 65.6% 84.2%, #fff 84.7% 85.1%, #ffb547 85.6% 99.2%, #fff 99.7%);
}
/* the control zone: the same object, at the same size, in every model that has one — so it is
written in plain vmin and not in the donut's own unit. Here it holds the legend. */
.controls {
display: grid;
justify-items: center;
gap: 2vmin;
text-align: center;
}
.controls .caption {
margin: 0;
font: 500 4.5vmin/1.2 Inter, system-ui, sans-serif;
opacity: 0.7;
}
.controls .row {
display: flex;
gap: 2.5vmin;
margin: 0;
padding: 0;
list-style: none;
}
/* a legend entry reports, it is not pressed: the row's height and text, no pill */
.legend li {
display: flex;
align-items: center;
gap: 1.2vmin;
height: 8vmin;
font: 600 4vmin Inter, system-ui, sans-serif;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.legend li::before {
content: '';
width: 2.8vmin;
height: 2.8vmin;
border-radius: 0.8vmin;
background: var(--c);
}
@keyframes spin {
to { transform: rotateZ(360deg); }
}