Data & tools
Flip clock
CSS + JSReal time needs JS. Each pair that changes re-triggers a CSS rotateX flip.
Each bar is a real cuboid — front, side and top face — growing on a staggered delay.
Your edited version
height — that runs layout on every frame. The bar’s box stays full size; the walls are squashed with scaleY from transform-origin: bottom.rotateX(90deg); it slides down with translateY by exactly the height the walls lost, so it stays sitting on top.preserve-3d inside flexboxscaleY walls + translateY lid (no layout)animation-delay stagger<div class="scene">
<div class="chart">
<!-- --hn = full height in px, as a plain number -->
<div class="bar" style="--hn:70; --hue:262; --delay:0s"><i></i><i></i><i></i></div>
<div class="bar" style="--hn:110; --hue:285; --delay:-0.35s"><i></i><i></i><i></i></div>
<div class="bar" style="--hn:50; --hue:320; --delay:-0.7s"><i></i><i></i><i></i></div>
<div class="bar" style="--hn:140; --hue:175; --delay:-1.05s"><i></i><i></i><i></i></div>
<div class="bar" style="--hn:90; --hue:40; --delay:-1.4s"><i></i><i></i><i></i></div>
</div>
</div>
.scene {
perspective: 900px;
}
.chart {
--w: 30px;
display: flex;
align-items: flex-end;
gap: 18px;
height: 150px;
transform-style: preserve-3d;
transform: rotateX(-22deg) rotateY(-32deg);
}
.bar {
--h: calc(var(--hn) * 1px); /* full height */
--k: calc(14 / var(--hn)); /* scale of the shortest state: 14px */
position: relative;
width: var(--w);
height: var(--h); /* the box itself never changes size */
transform-style: preserve-3d;
}
.bar i {
position: absolute;
top: 0;
left: 0;
width: var(--w);
height: 100%;
transform-origin: bottom center;
animation: 1.8s ease-in-out var(--delay) infinite alternate;
}
/* front */
.bar i:nth-child(1) {
background: hsl(var(--hue) 80% 60%);
transform: translateZ(calc(var(--w) / 2));
animation-name: grow-front;
}
/* right side */
.bar i:nth-child(2) {
background: hsl(var(--hue) 70% 42%);
transform: rotateY(90deg) translateZ(calc(var(--w) / 2));
animation-name: grow-side;
}
/* lid */
.bar i:nth-child(3) {
height: var(--w);
background: hsl(var(--hue) 90% 74%);
transform-origin: center;
transform: rotateX(90deg) translateZ(calc(var(--w) / 2));
animation-name: grow-lid;
}
/* each keyframe gives only the SHORT state; the tall state is the element's own transform */
@keyframes grow-front {
from { transform: translateZ(calc(var(--w) / 2)) scaleY(var(--k)); }
}
@keyframes grow-side {
from { transform: rotateY(90deg) translateZ(calc(var(--w) / 2)) scaleY(var(--k)); }
}
@keyframes grow-lid {
from { transform: translateY(calc(var(--h) - 14px)) rotateX(90deg) translateZ(calc(var(--w) / 2)); }
}
@use 'sass:list';
$w: 26px;
$min: 14; // shortest bar height in px (unitless so it can be divided by --hn)
$hues: (262, 285, 320, 175, 40);
.d-bars {
display: flex;
align-items: flex-end;
gap: 16px;
height: 130px;
transform-style: preserve-3d;
transform: translateY(10px) rotateX(-22deg) rotateY(-32deg);
// --hn is the full height as a plain number, set in the markup.
// The bar's box never changes size: animating `height` would run layout every frame.
// Instead the walls are squashed with scaleY and the lid slides down with translateY —
// transforms only, so the whole thing runs on the compositor.
&__bar {
--h: calc(var(--hn) * 1px);
--k: calc(#{$min} / var(--hn)); // scale factor of the shortest state
position: relative;
width: $w;
height: var(--h);
transform-style: preserve-3d;
@for $i from 1 through 5 {
&:nth-child(#{$i}) {
--hue: #{list.nth($hues, $i)};
--delay: #{$i * -0.35s};
}
}
i {
position: absolute;
top: 0;
left: 0;
width: $w;
height: 100%;
transform-origin: bottom center;
animation: 1.8s ease-in-out var(--delay) infinite alternate;
}
// front
i:nth-child(1) {
background: hsl(var(--hue) 80% 60%);
transform: translateZ($w * 0.5);
animation-name: d-bars-front;
}
// right side — a little darker
i:nth-child(2) {
background: hsl(var(--hue) 70% 42%);
transform: rotateY(90deg) translateZ($w * 0.5);
animation-name: d-bars-side;
}
// top — a $w × $w square laid flat, lightest
i:nth-child(3) {
height: $w;
background: hsl(var(--hue) 90% 74%);
transform-origin: center;
transform: rotateX(90deg) translateZ($w * 0.5);
animation-name: d-bars-top;
}
}
}
// Each keyframe only gives the SHORT state; the tall state is the element's own transform.
@keyframes d-bars-front {
from {
transform: translateZ($w * 0.5) scaleY(var(--k));
}
}
@keyframes d-bars-side {
from {
transform: rotateY(90deg) translateZ($w * 0.5) scaleY(var(--k));
}
}
@keyframes d-bars-top {
from {
transform: translateY(calc(var(--h) - #{$min * 1px})) rotateX(90deg) translateZ($w * 0.5);
}
}