Rotating word prism
Pure CSSFour 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.
A 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.
Your edited version
top/bottom show the current split, and fall/land are the ones that animate between the old and new character.content: attr(data-c), so JS never touches a text node — it only ever writes a data-c attribute..is-flip class, reads a layout property to force the browser to flush styles, then re-adds the class.animation-fill-mode: both matters: outside its delay the falling leaf sits parked edge-on at rotateX(-90deg), and the landing leaf holds its open pose until its own delay ends.--i staggers each cell by 45ms so the flips ripple across the row instead of firing together.two hinged half-leaves per cellcontent: attr(data-c)restart animation by re-adding a classanimation-fill-mode: both<div class="board">
<div class="board-head"><span>Departures</span><b>Gate 3D</b></div>
<div class="board-row" role="img" aria-label="Split-flap display cycling through city names">
<span class="cell" style="--i:0"><i class="cell-top"></i><i class="cell-bottom"></i><i class="cell-fall"></i><i class="cell-land"></i></span>
<span class="cell" style="--i:1"><i class="cell-top"></i><i class="cell-bottom"></i><i class="cell-fall"></i><i class="cell-land"></i></span>
<span class="cell" style="--i:2"><i class="cell-top"></i><i class="cell-bottom"></i><i class="cell-fall"></i><i class="cell-land"></i></span>
<span class="cell" style="--i:3"><i class="cell-top"></i><i class="cell-bottom"></i><i class="cell-fall"></i><i class="cell-land"></i></span>
<span class="cell" style="--i:4"><i class="cell-top"></i><i class="cell-bottom"></i><i class="cell-fall"></i><i class="cell-land"></i></span>
<span class="cell" style="--i:5"><i class="cell-top"></i><i class="cell-bottom"></i><i class="cell-fall"></i><i class="cell-land"></i></span>
<span class="cell" style="--i:6"><i class="cell-top"></i><i class="cell-bottom"></i><i class="cell-fall"></i><i class="cell-land"></i></span>
<span class="cell" style="--i:7"><i class="cell-top"></i><i class="cell-bottom"></i><i class="cell-fall"></i><i class="cell-land"></i></span>
</div>
</div>
.board {
display: grid;
gap: 8px;
padding: 12px 12px 10px;
border: 1px solid #3a4070;
border-radius: 12px;
background: linear-gradient(160deg, #211c3c, #0b0d18 70%);
box-shadow: 0 18px 30px -18px rgb(0 0 0 / 0.8);
}
.board-head {
display: flex;
align-items: center;
justify-content: space-between;
color: #949bc0;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.board-head b {
color: #ffb547;
font-weight: 800;
}
.board-row {
display: flex;
gap: 3px;
}
.cell {
position: relative;
width: 21px;
height: 34px;
perspective: 140px; /* its own close camera: a 17px leaf needs a strong one to read as falling */
}
/* the slit between the two halves */
.cell::after {
content: '';
position: absolute;
top: calc(50% - 0.5px);
right: 0;
left: 0;
height: 1px;
background: #05060c;
}
.cell i {
position: absolute;
right: 0;
left: 0;
height: 50%;
overflow: hidden; /* a leaf, so clipping here flattens nothing */
backface-visibility: hidden;
}
/* the whole character, twice the leaf's height; the leaf shows one half of it */
.cell i::before {
content: attr(data-c);
position: absolute;
right: 0;
left: 0;
height: 200%;
color: #f4f1e6;
font: 800 22px/34px ui-monospace, monospace;
text-align: center;
}
.cell-top,
.cell-fall {
top: 0;
border-radius: 4px 4px 0 0;
background: #1a1e36;
}
.cell-top::before,
.cell-fall::before {
top: 0;
}
/* bottom halves: slightly lighter, as if lit from above */
.cell-bottom,
.cell-land {
bottom: 0;
border-radius: 0 0 4px 4px;
background: #20254a;
}
.cell-bottom::before,
.cell-land::before {
bottom: 0;
}
.cell-fall {
transform: rotateX(-90deg); /* parked edge-on = invisible */
transform-origin: 50% 100%;
}
.cell-land {
transform-origin: 50% 0;
}
/* "both": during its delay the landing leaf waits edge-on at 90deg, afterwards it stays down
and simply covers the static bottom half */
.cell.is-flip .cell-fall {
animation: flap-fall 0.17s ease-in calc(var(--i) * 45ms) both;
}
.cell.is-flip .cell-land {
animation: flap-land 0.17s ease-out calc(var(--i) * 45ms + 0.17s) both;
}
@keyframes flap-fall {
from { transform: rotateX(0deg); }
to { transform: rotateX(-90deg); }
}
@keyframes flap-land {
from { transform: rotateX(90deg); }
to { transform: rotateX(0deg); }
}
var WORDS = ['NEW YORK', 'HELSINKI', 'LISBON', 'BANGKOK', 'SAN JOSE', 'CSS 3D'];
var cells = Array.prototype.map.call(document.querySelectorAll('.cell'), function (cell) {
return { cell: cell, leaves: cell.querySelectorAll('i'), char: ' ' }; // top, bottom, fall, land
});
function show(word, animate) {
var text = word + ' '; // pad so every cell always has a character
cells.forEach(function (c, i) {
var next = text[i];
if (next === c.char && animate) return;
var top = c.leaves[0];
var bottom = c.leaves[1];
var fall = c.leaves[2];
var land = c.leaves[3];
top.dataset.c = next; // revealed as the old top half falls
land.dataset.c = next; // lands on top of the old bottom half
fall.dataset.c = c.char;
bottom.dataset.c = c.char;
c.char = next;
if (!animate) return;
// restart the CSS animation: drop the class, force a style flush, add it again
c.cell.classList.remove('is-flip');
void c.cell.offsetWidth;
c.cell.classList.add('is-flip');
});
}
var index = 0;
show(WORDS[0], false);
setInterval(function () {
index = (index + 1) % WORDS.length;
show(WORDS[index], true);
}, 2600);
// Split-flap board. Every cell is four half-height leaves showing the top or bottom half of a
// character (the character itself comes from data-c, printed by ::before):
// top static, already shows the NEW character
// bottom static, still shows the OLD character
// fall top half of the OLD character, hinged on the middle, falls forward 0 -> -90deg
// land bottom half of the NEW character, hinged on the middle, lands 90deg -> 0
// JS only writes the data-c attributes and restarts the animation by re-adding .is-flip.
$w: 21px;
$h: 34px;
$dur: 0.17s;
.d-flaptext {
display: grid;
gap: 8px;
padding: 12px 12px 10px;
border: 1px solid var(--border-strong);
border-radius: 12px;
background: linear-gradient(160deg, color-mix(in srgb, var(--accent) 16%, #0b0d18), #0b0d18 70%);
box-shadow: 0 18px 30px -18px rgb(0 0 0 / 0.8);
&__head {
display: flex;
align-items: center;
justify-content: space-between;
color: #949bc0;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
b {
color: var(--warm);
font-weight: 800;
}
}
&__row {
display: flex;
gap: 3px;
}
&__cell {
position: relative;
width: $w;
height: $h;
// Its own, close camera: a 17px leaf needs a strong perspective to read as falling.
perspective: 140px;
// the slit between the two halves
&::after {
content: '';
position: absolute;
top: calc(50% - 0.5px);
right: 0;
left: 0;
height: 1px;
background: #05060c;
}
i {
position: absolute;
right: 0;
left: 0;
height: 50%;
overflow: hidden; // a leaf, so clipping here flattens nothing
backface-visibility: hidden;
// The whole character, twice the leaf's height; the leaf shows one half of it.
&::before {
content: attr(data-c);
position: absolute;
right: 0;
left: 0;
height: 200%;
color: #f4f1e6;
font: 800 22px / #{$h} var(--mono, ui-monospace, monospace);
text-align: center;
}
}
}
// top halves
&__top,
&__fall {
top: 0;
border-radius: 4px 4px 0 0;
background: #1a1e36;
&::before {
top: 0;
}
}
// bottom halves: slightly lighter, as if lit from above
&__bottom,
&__land {
bottom: 0;
border-radius: 0 0 4px 4px;
background: #20254a;
&::before {
bottom: 0;
}
}
&__fall {
transform: rotateX(-90deg); // parked edge-on = invisible
transform-origin: 50% 100%;
}
&__land {
transform-origin: 50% 0;
}
// `both`: during its delay the landing leaf waits edge-on at 90deg, afterwards it stays down
// and simply covers the static bottom half.
&__cell.is-flip &__fall {
animation: d-flaptext-fall $dur ease-in calc(var(--i) * 45ms) both;
}
&__cell.is-flip &__land {
animation: d-flaptext-land $dur ease-out calc(var(--i) * 45ms + #{$dur}) both;
}
}
@keyframes d-flaptext-fall {
from {
transform: rotateX(0deg);
}
to {
transform: rotateX(-90deg);
}
}
@keyframes d-flaptext-land {
from {
transform: rotateX(90deg);
}
to {
transform: rotateX(0deg);
}
}