Scene height & progress indicators
These two patterns extend the basic pinned scene: one makes a pinned scene's height follow however many items it actually holds, the other builds a thin bar that fills with the progress of any scene or scroll range.
Scene height that follows the item count
With 3 steps or with 12, each one should get the same amount of scroll to play its own animation. Rather than hard-coding a scene height, this recipe measures how many items are actually present and derives the right height from that — and recomputes the pinned window's progress for exactly that height.
Structure:
section .scene-section overflow-x reset, padding 0
└─ div .scene own Custom row, no Start/End (no "Get parent")
└─ div .scene__stage sticky (see Pinned scenes)
└─ … .scene__item ×n the items being counted, any depth
Children read the scene's --fa-scroll--custom by inheritance; they don't need a row of their
own.
CSS:
.scene {
position: relative;
width: 100%;
--sc-x: 4;
--sc-h: calc(var(--fa-scroll--custom) * 0vh + var(--sc-x) * 100vh);
height: var(--sc-h, auto);
}
.scene:has(.scene__item:nth-child(2)) { --sc-x: 2.2; }
.scene:has(.scene__item:nth-child(3)) { --sc-x: 2.8; }
.scene:has(.scene__item:nth-child(4)) { --sc-x: 3.4; }
.scene:has(.scene__item:nth-child(5)) { --sc-x: 4; }
.scene:has(.scene__item:nth-child(6)) { --sc-x: 4.6; }
/* … X = 1 + .6 × k, continuing up to k = 12 (X = 8.2); keep them in ascending order, the last match wins */
.scene__stage {
--sc-q: clamp(0, (var(--fa-scroll--custom) * (var(--sc-x) + 1) - 1) / (var(--sc-x) - 1), 1);
}
- A scene that's
Xscreens tall pins from progress1/(X+1)toX/(X+1).--sc-qremaps that exact stretch to a clean 0 → 1, for whateverXturns out to be. :has(.scene__item:nth-child(k))matches whenever there are at leastkitems. Without:has()support,Xjust stays at its default of 4.- Without the animation running at all,
--sc-hbecomes invalid and the scene falls back to its natural height — the stage simply isn't pinned, and its content flows like a normal section. - Item
i(from 0) ofntotal is "current" while--sc-q × nsits betweeniandi + 1. With "Index children" on the items' own parent,--fa-iand--fa-ngive youiandndirectly.
Values you can change: the .6 multiplier (screens of scroll per item), and the default
--sc-x (4, used as a fallback by browsers without :has() support).
- Bricks
- Etch
- Scene: add one Custom row with no Start/End. If its children need scroll smoothing, set it here too, since they inherit the smoothed value.
- The
:has()rules go in the scene's own custom CSS, referencing the item's real class name.
- Scene classes:
fa--custom(optionallyfa--smooth-20). - Write the rules nested inside
.scene's own class:&:has(.scene__item:nth-child(2)) { --sc-x: 2.2; }, and so on.
Common problems:
- Progress tuned by eye drifts the moment an item is added or removed. Windows were hard-coded
rather than derived from
--sc-qand the item's index — derive everything from those instead. - The stage gets cut off on short screens in the builder. A fixed height was used instead of
the
var(--sc-h, auto)fallback pattern. --sc-qlooks wrong. The scene element itself was transformed, or is being measured through "Get parent" from somewhere else — it must measure itself, and stay untransformed.
Progress indicator
A thin bar that fills from 0% to 100% as a scene plays, or as a long list scrolls past a sticky side column.
Structure (horizontal, inside a pinned scene):
div .scroll-progress centres the bar
└─ div .scroll-progress__bar the track: fixed size, overflow hidden
└─ div .scroll-progress__fill "Get parent" + one Custom row — the same window as the animation it reports on
Structure (vertical, beside a list that scrolls normally):
section .list-progress "I am parent", overflow-x reset
└─ div .list-progress__row flex row
├─ div .list-progress__content the long list
└─ div .list-progress__side sticky column, one screen tall
└─ .scroll-progress__bar > .scroll-progress__fill "Get parent" + Custom, Start 20, End 80
CSS:
.scroll-progress { display: flex; justify-content: center; }
.scroll-progress__bar {
width: 300px; height: 3px;
background: #d4d4d8; border-radius: 10em; overflow: hidden;
}
.scroll-progress__fill {
height: 100%; min-width: 0;
background: var(--primary, #2563eb);
width: calc(var(--fa-scroll--custom, 0) * 100%);
}
/* vertical variant */
.list-progress__row { display: flex; justify-content: space-between; }
.list-progress__content { width: calc(100% - 4rem); }
.list-progress__side {
position: sticky; top: 0; height: 100vh; width: 1rem;
display: flex; align-items: center; justify-content: center;
}
.list-progress__side .scroll-progress__bar { width: 4px; height: 180px; }
.list-progress__side .scroll-progress__fill { width: 100%; min-height: 0; height: calc(var(--fa-scroll--custom, 0) * 100%); }
The fill element can also skip having a row of its own and simply inherit --fa-scroll--custom
from an ancestor that already has one — the value is the same either way.
Values you can change: use the same Start/End (and the same scroll smoothing value) as whatever animation the bar is reporting on, or the two will visibly drift apart. For the pinned horizontal track specifically, use the progress bar built into that recipe instead — its scene height tracks the cards directly, so a fixed window here would drift from it.
- Bricks
- Etch
- Fill: "Get parent" + one Custom row with the Start/End you're reporting on.
- Bar and fill are empty elements — give their classes an explicit minimum width/height of 0, or the builder canvas shows them at its default 90px placeholder size.
- Vertical variant: the section needs "I am parent" and the overflow-x reset; the
.list-progress__side …descendant rules go in the side column's own custom CSS, written out in full.
- Fill classes:
fa--parent fa--custom fa--custom--start-25 fa--custom--end-75(adjust to the window you're reporting on). - Empty elements show as a placeholder box in the canvas — set the canvas-only empty-element size variable on the bar's class if that's distracting.
- Nest descendant rules inside the side column's class:
& .scroll-progress__fill { … }.
Common problems:
- The bar is full from the very start, or never moves at all. No "Get parent" (so it's measuring itself, which inside a pinned stage never changes), or no "I am parent" above it.
- It runs ahead of, or behind, the animation it's meant to track. A different window, or a different scroll smoothing value, than the thing it's reporting on.
- The side column doesn't stick. Missing overflow-x reset on the section, or the column isn't a full screen tall.
- The fill shows a visible sliver even at 0%. A builder default minimum width/height on an empty element — set it to 0 explicitly.