3D equalizer
Pure CSSSeven real cuboids bouncing like an audio meter. Walls are squashed with scaleY and each lid rides down by the same amount, so nothing but transforms ever changes.
Eight glowing dots with comet trails circle a core on three tilted orbits, while the whole atom slowly turns. Every dot counter-rotates so it always faces you, yet still passes behind the core.
Your edited version
rotateX(72deg) and turned with rotateZ(--phi); the dots live inside it, so they share its tilt and pass in front of and behind the core in real depth.rotateZ(θ) translateX(r) rotateZ(−θ): step out to the orbit, go round, and turn back by the same angle, so it travels without spinning. Then rotateX(-72deg) rotateZ(-phi) undoes the orbit's tilt so it faces the viewer.--lag), each smaller and fainter. Every delay has one full lap subtracted, so all of them are negative and no dot waits at the centre when the page loads.linear, so the loop has no seam; three lap times (1.6s, 2.4s, 3.3s) and one reversed ring keep it from ever looking in sync.rotateZ(θ) translateX(r) rotateZ(−θ): orbit without turningundo the plane’s tilt = billboardtrail copies = same animation, a little latervar() inside @keyframes<div class="scene">
<div class="atom" role="img" aria-label="Loading">
<b></b>
<div class="orbit" style="--c:#2ee6d6; --dur:1.6s; --dir:normal; --phi:0deg">
<i style="--p:0.000; --lag:0"></i>
<i style="--p:0.000; --lag:1"></i>
<i style="--p:0.000; --lag:2"></i>
<i style="--p:0.333; --lag:0"></i>
<i style="--p:0.333; --lag:1"></i>
<i style="--p:0.333; --lag:2"></i>
<i style="--p:0.667; --lag:0"></i>
<i style="--p:0.667; --lag:1"></i>
<i style="--p:0.667; --lag:2"></i>
</div>
<div class="orbit" style="--c:#ff4d9d; --dur:2.4s; --dir:reverse; --phi:60deg">
<i style="--p:0.000; --lag:0"></i>
<i style="--p:0.000; --lag:1"></i>
<i style="--p:0.000; --lag:2"></i>
<i style="--p:0.333; --lag:0"></i>
<i style="--p:0.333; --lag:1"></i>
<i style="--p:0.333; --lag:2"></i>
<i style="--p:0.667; --lag:0"></i>
<i style="--p:0.667; --lag:1"></i>
<i style="--p:0.667; --lag:2"></i>
</div>
<div class="orbit" style="--c:#ffb547; --dur:3.3s; --dir:normal; --phi:120deg">
<i style="--p:0.000; --lag:0"></i>
<i style="--p:0.000; --lag:1"></i>
<i style="--p:0.000; --lag:2"></i>
<i style="--p:0.500; --lag:0"></i>
<i style="--p:0.500; --lag:1"></i>
<i style="--p:0.500; --lag:2"></i>
</div>
</div>
</div>
.scene {
perspective: 800px;
}
/* the whole atom turns slowly; everything flat inside turns back by the same amount */
.atom {
position: relative;
width: 152px;
height: 152px;
transform-style: preserve-3d;
animation: atom-turn 16s linear infinite;
}
/* the core */
.atom b {
position: absolute;
inset: calc(50% - 20px);
border-radius: 50%;
background: radial-gradient(circle at 40% 36%, #fff 0 7%, #c5b6ff 20%, #8b6cff 46%, #463680 70%, transparent 72%);
animation: atom-face 16s linear infinite;
}
/* a soft halo that breathes (a gradient, no blur) */
.atom b::before {
content: '';
position: absolute;
inset: -16px;
border-radius: 50%;
background: radial-gradient(closest-side, rgb(139 108 255 / 0.45), transparent);
animation: atom-breathe 1.6s ease-in-out infinite alternate;
}
/* an orbit: a circle laid over by 72°, its axis turned by --phi */
.orbit {
position: absolute;
inset: 0;
border-radius: 50%;
transform-style: preserve-3d;
transform: rotateZ(var(--phi)) rotateX(72deg);
}
/* the dashed track */
.orbit::before {
content: '';
position: absolute;
inset: 0;
border: 1px dashed color-mix(in srgb, var(--c) 45%, transparent);
border-radius: 50%;
}
/* a dot: an invisible carrier going round; --p spaces the dots, --lag makes the trail
(same animation, a little later). One whole lap is subtracted so every delay is negative. */
.orbit i {
position: absolute;
top: calc(50% - 6px);
left: calc(50% - 6px);
width: 12px;
height: 12px;
transform-style: preserve-3d;
animation: dot-orbit var(--dur) linear infinite var(--dir);
animation-delay: calc(var(--lag) * var(--dur) * 0.017 - var(--p) * var(--dur) - var(--dur));
}
/* the glow; trail copies are smaller and fainter */
.orbit i::before {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: radial-gradient(circle,
color-mix(in srgb, var(--c) 30%, #fff) 0 7%,
var(--c) 24% 38%,
color-mix(in srgb, var(--c) 30%, transparent) 56%,
transparent 70%);
opacity: calc(1 - var(--lag) * 0.3);
scale: calc(1.25 - var(--lag) * 0.25);
animation: atom-face 16s linear infinite;
}
@keyframes atom-turn {
from { transform: rotateX(-16deg) rotateY(0deg); }
to { transform: rotateX(-16deg) rotateY(360deg); }
}
/* exactly undoes atom-turn: same duration, reversed order and signs */
@keyframes atom-face {
from { transform: rotateY(0deg) rotateX(16deg); }
to { transform: rotateY(-360deg) rotateX(16deg); }
}
/* read right to left: undo the orbit's tilt, cancel the lap, step out, go round */
@keyframes dot-orbit {
from { transform: rotateZ(0deg) translateX(76px) rotateZ(0deg) rotateX(-72deg) rotateZ(calc(var(--phi) * -1)); }
to { transform: rotateZ(360deg) translateX(76px) rotateZ(-360deg) rotateX(-72deg) rotateZ(calc(var(--phi) * -1)); }
}
@keyframes atom-breathe {
from { opacity: 0.55; transform: scale(0.85); }
to { opacity: 1; transform: scale(1.1); }
}
@use 'sass:list';
$r: 76px; // orbit radius
$turn: 16s; // one slow turn of the whole atom
$tilt: -16deg; // we look at it from a little above
// [colour, lap time, direction, where the ring's axis points (around Z)]
$rings: (
(var(--accent-2), 1.6s, normal, 0deg),
(var(--hot), 2.4s, reverse, 60deg),
(var(--warm), 3.3s, normal, 120deg)
);
// The whole atom turns slowly around Y. Everything flat inside it (the core, each dot) turns back
// by exactly as much, so it always faces the viewer instead of going edge-on.
.d-dotorbit {
position: relative;
width: $r * 2;
height: $r * 2;
transform-style: preserve-3d;
animation: d-dotorbit-turn $turn linear infinite;
// the core
b {
position: absolute;
inset: calc(50% - 20px);
border-radius: 50%;
background: radial-gradient(
circle at 40% 36%,
#fff 0 7%,
color-mix(in srgb, var(--accent) 45%, #fff) 20%,
var(--accent) 46%,
color-mix(in srgb, var(--accent) 50%, #000) 70%,
transparent 72%
);
animation: d-dotorbit-face $turn linear infinite;
// a soft halo that breathes (a gradient, no blur)
&::before {
content: '';
position: absolute;
inset: -16px;
border-radius: 50%;
background: radial-gradient(closest-side, color-mix(in srgb, var(--accent) 45%, transparent), transparent);
animation: d-dotorbit-breathe 1.6s ease-in-out infinite alternate;
}
}
// An orbit: a circle laid over by 72° (an ellipse to us), its axis turned by --phi.
&__ring {
position: absolute;
inset: 0;
border-radius: 50%;
transform-style: preserve-3d;
transform: rotateZ(var(--phi)) rotateX(72deg);
@for $k from 1 through list.length($rings) {
$ring: list.nth($rings, $k);
&:nth-of-type(#{$k}) {
--c: #{list.nth($ring, 1)};
--dur: #{list.nth($ring, 2)};
--dir: #{list.nth($ring, 3)};
--phi: #{list.nth($ring, 4)};
}
}
// the dashed track
&::before {
content: '';
position: absolute;
inset: 0;
border: 1px dashed color-mix(in srgb, var(--c) 45%, transparent);
border-radius: 50%;
}
// A dot is an invisible carrier that travels round the orbit; its ::before is the glow.
// --p spaces the dots round the ring, --lag (0, 1, 2) makes the trail: the same animation a
// little later, so it is always a little behind. One whole cycle is subtracted so every delay
// is negative and nothing waits at the centre before it starts.
i {
position: absolute;
top: calc(50% - 6px);
left: calc(50% - 6px);
width: 12px;
height: 12px;
transform-style: preserve-3d;
animation: d-dotorbit-orbit var(--dur) linear infinite var(--dir);
animation-delay: calc(var(--lag) * var(--dur) * 0.017 - var(--p) * var(--dur) - var(--dur));
&::before {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: radial-gradient(
circle,
color-mix(in srgb, var(--c) 30%, #fff) 0 7%,
var(--c) 24% 38%,
color-mix(in srgb, var(--c) 30%, transparent) 56%,
transparent 70%
);
opacity: calc(1 - var(--lag) * 0.3);
scale: calc(1.25 - var(--lag) * 0.25);
animation: d-dotorbit-face $turn linear infinite;
}
}
}
}
@keyframes d-dotorbit-turn {
from {
transform: rotateX($tilt) rotateY(0deg);
}
to {
transform: rotateX($tilt) rotateY(360deg);
}
}
// exactly undoes d-dotorbit-turn (same duration, reversed order and signs)
@keyframes d-dotorbit-face {
from {
transform: rotateY(0deg) rotateX(-$tilt);
}
to {
transform: rotateY(-360deg) rotateX(-$tilt);
}
}
// Read right to left: undo the ring's tilt and turn (so the dot faces out of the atom again),
// cancel the lap (rotateZ(-θ)), step out to the orbit, then go round it (rotateZ(θ)).
@keyframes d-dotorbit-orbit {
from {
transform: rotateZ(0deg) translateX($r) rotateZ(0deg) rotateX(-72deg) rotateZ(calc(var(--phi) * -1));
}
to {
transform: rotateZ(360deg) translateX($r) rotateZ(-360deg) rotateX(-72deg) rotateZ(calc(var(--phi) * -1));
}
}
@keyframes d-dotorbit-breathe {
from {
opacity: 0.55;
transform: scale(0.85);
}
to {
opacity: 1;
transform: scale(1.1);
}
}