Layout — make room
Variant: Make room
Insertion in the honest order: the grid parts first, the empty slot holds for 130ms, and only then does the newcomer spend its 286ms arriving — scale 0.94 and a fade, decelerating, because an entrance is a response and nothing appears from nothing. Reverse the sequence and the card lands on neighbours still in motion, which is a collision however brief. The same leading gap a drag-reorder opens 32ms early, taken to its conclusion: the room exists before the thing that needs it. Use it when items arrive into a visible collection — a card added to a board, a result inserted into place — and the user should see where it went before it gets there.
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Layout — make room</title>
<style>
:root {
/* Neutrals — fixed, never remixed. */
--ivory: #faf9f5;
--paper: #ffffff;
--oat: #e3dacc;
--slate: #141413;
/* Knobs — the remix API. A knob exists iff declared here and used below. */
--hue: 38.8;
--lift: 0;
--chroma: 1;
--round: 1;
--speed: 1;
/* Accents — derived from the knobs with the family's offsets baked in,
so one hue roll moves them together. Defaults reproduce the original hex. */
--clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
--clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
--olive: oklch(
calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3)
);
--rust: oklch(
calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3)
);
/* Clock — 5.2s. The grid parts over 364ms, holds the empty slot for
130ms, and only then does the newcomer spend its 286ms arriving. */
--cycle: 5.2s;
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
/* Slot pitch — card plus gap, so every travel lands in a slot. */
--_sw: 72px;
--_sh: 48px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: var(--ivory);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
color: var(--slate);
}
.grid {
position: relative;
width: 208px;
height: 88px;
animation: scene var(--t) linear infinite;
}
.card {
position: absolute;
width: 64px;
height: 40px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 6px;
padding: 0 9px;
border-radius: calc(7px * var(--round));
background: var(--paper);
border: 1px solid color-mix(in srgb, var(--slate) 10%, transparent);
}
/* Five incumbents packed into the first five slots; the newcomer is
parked in the slot the grid is about to open for it. */
.c1 { left: 0; top: 0; }
.c3 { left: 72px; top: 0; }
.c4 { left: 144px; top: 0; }
.c5 { left: 0; top: 48px; }
.c6 { left: 72px; top: 48px; }
.new { left: 72px; top: 0; }
.dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: color-mix(in srgb, var(--slate) 26%, transparent);
}
.new .dot {
background: var(--clay);
}
.bar {
height: 5px;
border-radius: 2.5px;
background: color-mix(in srgb, var(--slate) 15%, transparent);
}
.c1 .bar { width: 38px; }
.c3 .bar { width: 44px; }
.c4 .bar { width: 30px; }
.c5 .bar { width: 42px; }
.c6 .bar { width: 34px; }
.new .bar { width: 26px; }
.new {
opacity: 0;
transform: scale(0.94);
animation: arrive var(--t) infinite;
}
.c3 {
animation: shift-right var(--t) infinite;
}
/* The wrap traveller drops down a row along one straight diagonal, and
rides above the grid because its path is the only one that crosses
occupied ground. */
.c4 {
z-index: 2;
animation: wrap-down var(--t) infinite;
}
.c5,
.c6 {
animation: shift-right var(--t) infinite;
}
/* The room is fully open before the newcomer begins to exist. Reverse the
order and the card lands on top of a neighbour still in motion — an
arrival into occupied space is a collision, however brief. The same
idea as a gap that leads a drag, taken to its conclusion. */
@keyframes shift-right {
0%,
12% {
transform: translate(0, 0);
animation-timing-function: var(--ease-in-out);
}
19%,
100% {
transform: translate(var(--_sw), 0);
}
}
@keyframes wrap-down {
0%,
12% {
transform: translate(0, 0);
animation-timing-function: var(--ease-in-out);
}
19%,
100% {
transform: translate(calc(-2 * var(--_sw)), var(--_sh));
}
}
/* Scale 0.94 to 1 with the fade — nothing appears from nothing, and an
entrance decelerates because it is a response arriving, not a launch. */
@keyframes arrive {
0%,
21.5% {
opacity: 0;
transform: scale(0.94);
animation-timing-function: var(--ease-out);
}
27%,
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes scene {
0% {
opacity: 0;
}
3.5%,
80% {
opacity: 1;
}
87%,
100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
/* The parting keeps its distance and gives up its glide: steps(1)
opens the slot on one frame. The newcomer keeps only its fade —
the opacity is the announcement; the scale was just manners. */
.c3,
.c5,
.c6 {
animation-name: shift-right-step;
}
.c4 {
animation-name: wrap-down-step;
}
.new {
transform: none;
animation-name: arrive-still;
}
.grid,
.new,
.c3,
.c4,
.c5,
.c6 {
animation-duration: calc(var(--t) * 1.5);
}
@keyframes arrive-still {
0%,
21.5% {
opacity: 0;
animation-timing-function: ease;
}
27%,
100% {
opacity: 1;
}
}
@keyframes shift-right-step {
0%,
12% {
transform: translate(0, 0);
animation-timing-function: steps(1, end);
}
19%,
100% {
transform: translate(var(--_sw), 0);
}
}
@keyframes wrap-down-step {
0%,
12% {
transform: translate(0, 0);
animation-timing-function: steps(1, end);
}
19%,
100% {
transform: translate(calc(-2 * var(--_sw)), var(--_sh));
}
}
}
</style>
</head>
<body>
<div
class="grid"
role="img"
aria-label="A grid parts to open an empty slot, holds it for a beat, and a new card fades and scales into the room made for it"
>
<div class="card c1"><span class="dot"></span><span class="bar"></span></div>
<div class="card c3"><span class="dot"></span><span class="bar"></span></div>
<div class="card c4"><span class="dot"></span><span class="bar"></span></div>
<div class="card c5"><span class="dot"></span><span class="bar"></span></div>
<div class="card c6"><span class="dot"></span><span class="bar"></span></div>
<div class="card new"><span class="dot"></span><span class="bar"></span></div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.