Horizontal rail
Vertical scroll travels a horizontal track through a pinned stage.
01
02
03
04
05
<div data-sv data-sv-pin="300vh">
<div class="sv-stage flex items-center">
<div class="sv-rail flex gap-4 px-[10vw]">
{cards.map(c => <Card key={c.id} class="w-80 shrink-0" />)}
</div>
</div>
</div>
<div data-sv data-sv-pin="300vh"> <!-- the pin helper owns the height -->
<div class="sv-stage rail-stage"> <!-- the sticky viewport, from pin.css -->
<div class="sv-rail">…cards…</div>
</div>
</div>
/* the preset (styles/pin.css), plus the stage layout, which is yours: */
.rail-stage { display: flex; align-items: center; }
/* --sv-stage-width is measured by the driver; --sv-rail-start overrides it */
.sv .sv-rail { width: max-content; display: flex; gap: 1rem; padding: 0 10vw;
translate: calc((1 - var(--sv-pin, 0)) * var(--sv-rail-start, var(--sv-stage-width, 100vw)) +
var(--sv-pin, 0) * min(var(--sv-stage-width, 100vw) - 100%, 0px)) 0; }
/* the same sheet's reduced-motion override, last so it wins on source order: */
@media (prefers-reduced-motion: reduce) {
.sv .sv-rail { translate: none; width: auto; flex-wrap: wrap; }
}
/* the same under html[data-sv-motion="reduce"], the site's own switch */
:where([data-sv-motion="reduce"]) .sv .sv-rail { translate: none; width: auto; flex-wrap: wrap; }
<Track pin="300vh">
<div className="sv-stage flex items-center">
<div className="sv-rail flex gap-4 px-[10vw]">{cards}</div>
</div>
</Track>