3D bar chart
Pure CSSEach bar is a real cuboid — front, side and top face — growing on a staggered delay.
Sliders feed perspective and rotation straight into CSS, with the resulting declaration shown live.
Your edited version
perspective is the distance from your eye to the z=0 plane. Low values (≈150px) distort wildly; high values (≈1200px) look almost flat.perspectiverotateX / rotateYrange inputs → custom properties<div class="play">
<div class="view"><div class="box">3D</div></div>
<label>perspective <input type="range" data-var="p" data-unit="px" min="120" max="1200" value="400"></label>
<label>rotateX <input type="range" data-var="rx" data-unit="deg" min="-80" max="80" value="20"></label>
<label>rotateY <input type="range" data-var="ry" data-unit="deg" min="-80" max="80" value="-35"></label>
<code class="out"></code>
</div>
.play {
display: grid;
gap: 10px;
width: min(360px, 90vw);
font: 14px ui-monospace, monospace;
}
.view {
display: grid;
place-items: center;
height: 260px;
perspective: var(--p, 400px);
}
.box {
display: grid;
place-items: center;
width: 150px;
height: 150px;
border-radius: 16px;
font: 900 2.4rem system-ui;
color: #fff;
background: linear-gradient(135deg, #ffb547, #ff4d9d);
transform: rotateX(var(--rx, 20deg)) rotateY(var(--ry, -35deg));
}
label { display: grid; color: #949bc0; }
input { accent-color: #ffb547; }
.out { color: #2ee6d6; }
const root = document.querySelector('.play');
const out = root.querySelector('.out');
const inputs = [...root.querySelectorAll('input[type=range]')];
function update() {
const v = {};
for (const input of inputs) {
v[input.dataset.var] = input.value + input.dataset.unit;
root.style.setProperty('--' + input.dataset.var, v[input.dataset.var]);
}
out.textContent =
'perspective: ' + v.p + '; transform: rotateX(' + v.rx + ') rotateY(' + v.ry + ');';
}
root.addEventListener('input', update);
update();
.d-play {
display: grid;
grid-template-rows: 1fr auto auto;
width: 100%;
height: 100%;
padding: 10px 14px;
// Its own perspective container, so the slider value is the only one in play.
&__view {
display: grid;
place-items: center;
perspective: var(--p, 400px);
}
&__box {
display: grid;
place-items: center;
width: 110px;
height: 110px;
border-radius: 14px;
background: linear-gradient(135deg, var(--warm), var(--hot));
color: #fff;
font-size: 30px;
font-weight: 900;
box-shadow: 0 20px 30px -16px var(--hot);
transform: rotateX(var(--rx, 20deg)) rotateY(var(--ry, -35deg));
}
&__controls {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
width: min(100%, 420px); // centred under the card, like every control row
margin-inline: auto;
label {
display: grid;
color: var(--muted);
font-family: var(--mono);
font-size: 11px;
}
input {
width: 100%;
accent-color: var(--warm);
}
}
&__out {
margin-top: 4px;
overflow: hidden;
color: var(--accent-2);
font-family: var(--mono);
font-size: 10.5px;
text-overflow: ellipsis;
white-space: nowrap;
}
}