3D bar chart
Pure CSSEach bar is a real cuboid — front, side and top face — growing on a staggered delay.
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.
Your edited version
conic-gradient with hard stops at the running totals (40%, 65%, 85%). Repeat it in twelve layers, each translateZ(var(--i) * 1.5px) higher.--i shrinks shades that wall.radial-gradient mask on each layer. A mask on the spinning parent would flatten all twelve layers into one picture.rotateZ inside the tilt, so every slice passes the front in turn.conic-gradient segmentsstacked translateZ layersdarkening by --imask on the leaf layers only<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>
.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); }
}
// 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);
}
}