Split-flap board
CSS + JSA departure board: each character is four half-height leaves, and a change drops the old top half and lands the new bottom half. JS only swaps the letters and restarts the CSS animation.
Four words on the four long sides of a prism that turns a quarter at a time, pausing on each. After 360° it is back where it started, so the loop has no seam.
Your edited version
n is turned n quarter turns backwards around X, then pushed out with translateZ by half the prism’s depth — the same "turn, then push" recipe as a basic cube, just on one axis.translateZ(-18px)) so the face currently in front sits exactly at z = 0 and stays crisp.cubic-bezier — a snappy, springy turn rather than a constant spin.backface-visibility: hidden stops a word from showing mirrored through the box while it turns.360deg looks exactly like 0deg, so the loop has no seam.rotateX(n × -90deg) translateZ(h / 2)hold + turn keyframesbackface-visibility: hiddenper-keyframe easing<div class="scene">
<div class="wordcube">
<span>We make</span>
<span class="prism">
<span style="--i:0;--c:#8b6cff">design</span>
<span style="--i:1;--c:#2ee6d6">code</span>
<span style="--i:2;--c:#ff4d9d">motion</span>
<span style="--i:3;--c:#ffb547">brands</span>
</span>
</div>
</div>
.scene {
perspective: 800px;
}
.wordcube {
display: flex;
align-items: center;
gap: 9px;
color: #eceefb;
font-size: 21px;
font-weight: 800;
white-space: nowrap;
transform-style: preserve-3d;
}
.prism {
position: relative;
display: inline-block;
width: 104px;
height: 36px; /* face height = prism depth: its cross-section is a square */
transform-style: preserve-3d;
animation: cube-turn 9s infinite;
}
.prism span {
position: absolute;
inset: 0;
display: flex;
align-items: center;
padding-left: 12px;
border: 1px solid color-mix(in srgb, var(--c) 80%, transparent);
border-radius: 8px;
background: color-mix(in srgb, var(--c) 26%, transparent);
box-shadow: inset 0 0 18px color-mix(in srgb, var(--c) 32%, transparent);
color: var(--c);
backface-visibility: hidden;
transform: rotateX(calc(var(--i) * -90deg)) translateZ(18px);
}
@keyframes cube-turn {
0%, 19% {
transform: translateZ(-18px) rotateX(0deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
25%, 44% {
transform: translateZ(-18px) rotateX(90deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
50%, 69% {
transform: translateZ(-18px) rotateX(180deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
75%, 94% {
transform: translateZ(-18px) rotateX(270deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
100% {
transform: translateZ(-18px) rotateX(360deg);
}
}
$h: 36px; // height of a face = depth of the prism (its cross-section is a square)
.d-wordcube {
display: flex;
align-items: center;
gap: 9px;
color: var(--text);
font-size: 21px;
font-weight: 800;
white-space: nowrap;
transform-style: preserve-3d;
&__prism {
position: relative;
width: 104px;
height: $h;
transform-style: preserve-3d;
animation: d-wordcube-turn 9s infinite;
}
// Face n is turned n quarter turns BACKWARDS around X, then pushed out by half the depth:
// front, bottom, back, top. Turning the prism forwards brings them up one after another.
&__prism > span {
position: absolute;
inset: 0;
display: flex;
align-items: center;
padding-left: 12px;
border: 1px solid color-mix(in srgb, var(--c) 80%, transparent);
border-radius: 8px;
background: color-mix(in srgb, var(--c) 26%, transparent);
box-shadow: inset 0 0 18px color-mix(in srgb, var(--c) 32%, transparent);
color: var(--c);
backface-visibility: hidden; // never show a word mirrored through the prism
transform: rotateX(calc(var(--i) * -90deg)) translateZ($h * 0.5);
}
}
// Hold, quarter turn, hold... The easing is set per keyframe and only matters on the turns.
// The prism is pulled back by half its depth so the front face sits at z = 0 and stays crisp.
// 360deg looks exactly like 0deg, so the loop is seamless.
@keyframes d-wordcube-turn {
0%,
19% {
transform: translateZ($h * -0.5) rotateX(0deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
25%,
44% {
transform: translateZ($h * -0.5) rotateX(90deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
50%,
69% {
transform: translateZ($h * -0.5) rotateX(180deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
75%,
94% {
transform: translateZ($h * -0.5) rotateX(270deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
100% {
transform: translateZ($h * -0.5) rotateX(360deg);
}
}