Back to the library
Pilotis panel lift, bottom-up stagger
Variant: Bottom-up stagger
Same panel, same 260ms per-element entrance, but the parts arrive in construction order: the five supports cascade left to right at 50ms apart, then the slab, then the ribbon window, then the roof. Use it when the panel is genuinely assembled from separate data — a list of stats, a toolbar — so the cascade explains structure rather than decorating it. Total sequence lands under 700ms so the panel never feels slow to fill.
entrancestaggercascadearchitecturalloop
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>Pilotis panel lift — bottom-up stagger</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
(#d97757 / #b85c3e / #788c5d / #b04a3f). */
--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 — every duration in the document runs off --t. */
--cycle: 3.2s;
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--_enter: calc(0.26s / var(--speed));
--_step: calc(0.05s / var(--speed)); /* per-pier offset */
--_beat: calc(0.09s / var(--speed)); /* offset between structural layers */
}
* { box-sizing: border-box; }
html, body { margin: 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);
padding: clamp(10px, 4vw, 24px);
}
.stage {
width: min(400px, 90vw);
display: grid;
gap: clamp(8px, 2.4vw, 12px);
}
.panel {
position: relative;
background: var(--paper);
border: 1px solid var(--oat);
border-radius: calc(4px * var(--round));
padding: clamp(12px, 3.6vw, 18px);
display: grid;
gap: clamp(9px, 2.6vw, 13px);
}
.roof {
height: clamp(8px, 2.4vw, 10px);
border-radius: calc(2px * var(--round));
background: var(--olive);
transform-origin: 0% 100%;
animation: roof-set var(--_enter) var(--ease-out) infinite;
}
.slab {
height: clamp(46px, 15vw, 62px);
border-radius: calc(3px * var(--round));
background:
linear-gradient(90deg, var(--clay) 0 38%, var(--clay-d) 38% 100%);
animation: slab-lift var(--_enter) var(--ease-out) infinite;
}
.ribbon {
position: absolute;
left: clamp(12px, 3.6vw, 18px);
right: clamp(12px, 3.6vw, 18px);
top: calc(clamp(12px, 3.6vw, 18px) + clamp(8px, 2.4vw, 10px) + clamp(9px, 2.6vw, 13px) + clamp(14px, 4.4vw, 19px));
height: clamp(14px, 4vw, 18px);
background: var(--ivory);
border-radius: calc(2px * var(--round));
transform-origin: 0% 50%;
animation: ribbon-open var(--_enter) var(--ease-out) infinite;
}
.piers {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: clamp(10px, 3.2vw, 16px);
height: clamp(20px, 6vw, 26px);
}
.pier {
background: var(--slate);
border-radius: calc(2px * var(--round));
transform-origin: 50% 100%;
animation: pier-raise var(--_enter) var(--ease-out) infinite;
}
/* Sequence: piers cascade, then slab, ribbon, roof.
Each element runs a short finite-length keyframe with a negative-free
delay, and the whole set is restarted by the shared --t master loop
via animation-delay stacking against the wrapper's hold. */
.panel {
animation: panel-cycle var(--t) linear infinite;
}
.pier:nth-child(1) { animation-delay: 0s; }
.pier:nth-child(2) { animation-delay: var(--_step); }
.pier:nth-child(3) { animation-delay: calc(var(--_step) * 2); }
.pier:nth-child(4) { animation-delay: calc(var(--_step) * 3); }
.pier:nth-child(5) { animation-delay: calc(var(--_step) * 4); }
.slab { animation-delay: calc(var(--_step) * 4 + var(--_beat)); }
.ribbon { animation-delay: calc(var(--_step) * 4 + var(--_beat) * 2); }
.roof { animation-delay: calc(var(--_step) * 4 + var(--_beat) * 3); }
/* The panel wrapper owns the loop: it fades the whole assembly out at the
tail of the cycle so the staggered entrance has something to re-enter from. */
@keyframes panel-cycle {
0%, 88% { opacity: 1; }
96% { opacity: 0; }
100% { opacity: 0; }
}
.caption {
font-size: clamp(11px, 3vw, 12px);
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--slate);
opacity: 0.5;
text-align: center;
}
@keyframes slab-lift {
from { opacity: 0; transform: translate3d(0, 5%, 0) scale(0.97); }
to { opacity: 1; transform: none; }
}
@keyframes roof-set {
from { opacity: 0; transform: translate3d(0, 6px, 0) scaleX(0.94); }
to { opacity: 1; transform: none; }
}
@keyframes ribbon-open {
from { opacity: 0; transform: scaleX(0.04); }
to { opacity: 1; transform: scaleX(1); }
}
@keyframes pier-raise {
from { opacity: 0; transform: scaleY(0.35); }
to { opacity: 1; transform: scaleY(1); }
}
/* Elements hold their finished state between passes. */
.roof, .slab, .ribbon, .pier {
animation-iteration-count: 1;
animation-fill-mode: both;
}
/* Re-trigger the one-shot entrance every master cycle by tying the
children's animation to the wrapper's own restart. */
.panel {
animation-name: panel-cycle;
}
.panel .roof,
.panel .slab,
.panel .ribbon,
.panel .pier {
animation-duration: var(--_enter);
animation-iteration-count: infinite;
}
/* Infinite version of each entrance: the motion occupies the first slice,
the remainder is a hold, so the stagger reads once per master cycle. */
@keyframes slab-lift {
0% { opacity: 0; transform: translate3d(0, 5%, 0) scale(0.97); }
100% { opacity: 1; transform: none; }
}
.panel .roof,
.panel .slab,
.panel .ribbon,
.panel .pier {
animation-duration: var(--t);
}
/* Timeline percentages against --t (3.2s): 260ms ≈ 8%. */
@keyframes pier-raise {
0% { opacity: 0; transform: scaleY(0.35); }
8% { opacity: 1; transform: scaleY(1); }
100% { opacity: 1; transform: scaleY(1); }
}
@keyframes slab-lift {
0% { opacity: 0; transform: translate3d(0, 5%, 0) scale(0.97); }
8% { opacity: 1; transform: none; }
100% { opacity: 1; transform: none; }
}
@keyframes ribbon-open {
0% { opacity: 0; transform: scaleX(0.04); }
8% { opacity: 1; transform: scaleX(1); }
100% { opacity: 1; transform: scaleX(1); }
}
@keyframes roof-set {
0% { opacity: 0; transform: translate3d(0, 6px, 0) scaleX(0.94); }
8% { opacity: 1; transform: none; }
100% { opacity: 1; transform: none; }
}
@media (max-width: 380px) {
.panel { padding: 12px; }
.caption { letter-spacing: 0.1em; }
}
@media (prefers-reduced-motion: reduce) {
.panel { animation: none; opacity: 1; }
/* Stagger survives as a slow opacity relay — order without travel. */
.slab, .roof, .ribbon, .pier {
animation: soft-breathe calc(var(--cycle) * 2.4 / var(--speed)) ease-in-out infinite;
transform: none;
}
.pier:nth-child(1) { animation-delay: 0s; }
.pier:nth-child(2) { animation-delay: calc(var(--_step) * 2); }
.pier:nth-child(3) { animation-delay: calc(var(--_step) * 4); }
.pier:nth-child(4) { animation-delay: calc(var(--_step) * 6); }
.pier:nth-child(5) { animation-delay: calc(var(--_step) * 8); }
.slab { animation-delay: calc(var(--_step) * 10); }
.ribbon { animation-delay: calc(var(--_step) * 12); }
.roof { animation-delay: calc(var(--_step) * 14); }
@keyframes soft-breathe {
0%, 100% { opacity: 1; }
50% { opacity: 0.62; }
}
}
</style>
</head>
<body>
<div class="stage">
<div class="panel">
<div class="roof"></div>
<div class="slab"></div>
<div class="ribbon"></div>
<div class="piers">
<span class="pier"></span>
<span class="pier"></span>
<span class="pier"></span>
<span class="pier"></span>
<span class="pier"></span>
</div>
</div>
<p class="caption">Section ready</p>
</div>
</body>
</html>The variant family
Same idea, one axis moved. Compare them side by side before you commit.