<div><span class="lbl">Vertical scroll drives a horizontal track</span>
<section class="hsc hsc-pin" style="--hsc-count:7" aria-labelledby="hsc-pin-title">
<div class="hsc-pin-frame">
<div class="hsc-head">
<div><h2 id="hsc-pin-title">Selected work, 2021 to 2026</h2><p>Seven buildings by Studio Okapi, from a library in Bergen to a timber house in Ghent.</p></div>
<p class="hsc-hint hsc-hint-pinned">Scroll down to move along</p><p class="hsc-hint hsc-hint-swipe">Scroll sideways or use the arrow keys</p>
</div>
<div class="hsc-viewport" tabindex="0" role="region" aria-label="Selected work, scrolls sideways">
<ol class="hsc-track">
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1487958449943-2429e8be8625?auto=format&fit=crop&w=720&q=75" alt="White angular building with a glass base under a pale sky" width="720" height="900" loading="lazy"><figcaption><small>Bergen · 2025</small><b>Kvarteret Library</b><span>Public library, 6,200 m²</span></figcaption></figure></li>
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1479839672679-a46483c0e7c8?auto=format&fit=crop&w=720&q=75" alt="White apartment block with stacked, cantilevered balconies" width="720" height="900" loading="lazy"><figcaption><small>Lisbon · 2024</small><b>Alcântara Terraces</b><span>48 apartments</span></figcaption></figure></li>
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1518005020951-eccb494ad742?auto=format&fit=crop&w=720&q=75" alt="Curved white and blue facade seen from below" width="720" height="900" loading="lazy"><figcaption><small>Rotterdam · 2023</small><b>Maashaven Offices</b><span>Offices and a public roof</span></figcaption></figure></li>
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1524230572899-a752b3835840?auto=format&fit=crop&w=720&q=75" alt="A long white corridor of repeating arches" width="720" height="900" loading="lazy"><figcaption><small>Seville · 2023</small><b>Cloister Gallery</b><span>Museum extension</span></figcaption></figure></li>
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&fit=crop&w=720&q=75" alt="White villa with a pool and palm trees" width="720" height="900" loading="lazy"><figcaption><small>Mallorca · 2022</small><b>Casa Sóller</b><span>Private house</span></figcaption></figure></li>
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1496307653780-42ee777d4833?auto=format&fit=crop&w=720&q=75" alt="The tip of a white glass tower fading into a white sky" width="720" height="900" loading="lazy"><figcaption><small>Frankfurt · 2022</small><b>Linden Tower</b><span>Offices, 31 floors</span></figcaption></figure></li>
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=720&q=75" alt="Timber-clad family house with a deep porch and black side wing" width="720" height="900" loading="lazy"><figcaption><small>Ghent · 2021</small><b>Weaver's House</b><span>Private house</span></figcaption></figure></li>
</ol>
</div>
</div>
</section></div>
<div><span class="lbl">Scroll-snap gallery with buttons</span>
<section class="hsc" aria-labelledby="hsc-gal-title" data-hsc-gallery>
<div class="hsc-head">
<div><h2 id="hsc-gal-title">Interiors</h2><p>Rooms from recent projects. Swipe, scroll sideways or use the buttons.</p></div>
<div class="hsc-controls" hidden><button type="button" data-hsc-prev aria-label="Previous interiors">←</button><button type="button" data-hsc-next aria-label="Next interiors">→</button></div>
</div>
<div class="hsc-viewport" tabindex="0" role="region" aria-label="Interiors, scrolls sideways" data-hsc-scroller>
<ul class="hsc-track">
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=720&q=75" alt="Open living room with a white sofa, a timber wall and a kitchen beyond" width="720" height="900" loading="lazy"><figcaption><small>Living</small><b>Casa Sóller</b><span>Oak, lime plaster, terrazzo</span></figcaption></figure></li>
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1497366811353-6870744d04b2?auto=format&fit=crop&w=720&q=75" alt="Loft office with concrete ceiling, steel windows and a long table" width="720" height="900" loading="lazy"><figcaption><small>Workplace</small><b>Maashaven Offices</b><span>Exposed concrete, steel glazing</span></figcaption></figure></li>
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=720&q=75" alt="Long bright corridor with a dark kitchen wall and shelving" width="720" height="900" loading="lazy"><figcaption><small>Kitchen</small><b>Linden Tower, floor 31</b><span>Shared kitchen and lounge</span></figcaption></figure></li>
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=720&q=75" alt="Modern house at dusk with glowing floor-to-ceiling windows" width="720" height="900" loading="lazy"><figcaption><small>Evening</small><b>House in the Pines</b><span>Glass pavilion and terrace</span></figcaption></figure></li>
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1545324418-cc1a3fa10c00?auto=format&fit=crop&w=720&q=75" alt="Dark brick apartment building with balconies against a blue sky" width="720" height="900" loading="lazy"><figcaption><small>Housing</small><b>Nørrebro Yards</b><span>Brick, 64 homes</span></figcaption></figure></li>
<li class="hsc-card"><figure><img src="https://images.unsplash.com/photo-1431576901776-e539bd916ba2?auto=format&fit=crop&w=720&q=75" alt="Two gold glass towers seen from street level looking up" width="720" height="900" loading="lazy"><figcaption><small>Facade study</small><b>Gold Street Towers</b><span>Unbuilt competition entry</span></figcaption></figure></li>
</ul>
</div>
</section></div>
CSS
/* Horizontal scroll sections. By default each track is an ordinary horizontal scroll container with
scroll-snap, focusable so arrow keys scroll it. (1) Where scroll-driven animations are supported and motion is
allowed, the first section becomes tall, its frame sticks, and vertical page scrolling slides the track
sideways through a view() timeline. (2) The gallery stays a native snap scroller; a few lines of JavaScript
add previous/next buttons. */
.hsc{--hsc-ink:#171614;--hsc-muted:#57534e;--hsc-accent:#9a3412;--hsc-card:#fff;--hsc-radius:18px;--hsc-card-w:clamp(250px,30vw,360px);color:var(--hsc-ink)}
.hsc :where(h2,h3,p,ol,ul,figure){margin:0}
.hsc img{display:block;width:100%;height:auto;aspect-ratio:4/5;object-fit:cover}
.hsc-head{display:flex;flex-wrap:wrap;align-items:end;justify-content:space-between;gap:16px;margin-bottom:22px}
.hsc-head h2{font-size:clamp(1.6rem,1.2rem + 1.8vw,2.4rem);line-height:1.1;letter-spacing:-.03em}
.hsc-head p{margin-top:6px;max-width:48ch;color:var(--hsc-muted)}
.hsc-track{list-style:none;padding:0;display:flex;gap:18px;width:max-content}
.hsc-card{width:var(--hsc-card-w);flex:none;scroll-snap-align:start;border-radius:var(--hsc-radius);overflow:hidden;background:var(--hsc-card);box-shadow:0 20px 40px -32px rgba(23,22,20,.55)}
.hsc-card figcaption{padding:14px 18px 18px}
.hsc-card b{display:block;font-size:1.02rem}
.hsc-card span{font-size:.86rem;color:var(--hsc-muted)}
.hsc-card small{display:block;margin-bottom:2px;font-size:.72rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--hsc-accent)}
/* the scroller: native horizontal scrolling with snap points */
.hsc-viewport{overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x mandatory;scroll-padding-inline:4px;padding:4px 4px 18px;border-radius:var(--hsc-radius)}
.hsc-viewport:focus-visible{outline:3px solid var(--hsc-accent);outline-offset:3px}
@media (prefers-reduced-motion:no-preference){.hsc-viewport{scroll-behavior:smooth}}
/* 1. pinned section: vertical scroll drives the track */
.hsc-pin-frame{display:flex;flex-direction:column;justify-content:center}
.hsc-hint{font-size:.86rem;color:var(--hsc-muted)}
.hsc-hint-pinned{display:none}
@keyframes hsc-slide{to{transform:translateX(calc(-100% + 100cqi))}}
@supports (animation-timeline:view()){
@media (prefers-reduced-motion:no-preference){
.hsc-pin{height:calc(100vh + var(--hsc-count,6) * 0.45 * var(--hsc-card-w));view-timeline:--hsc-pin block}
.hsc-pin-frame{position:sticky;top:0;height:100vh;overflow:clip}
.hsc-pin .hsc-viewport{overflow:clip;container-type:inline-size;scroll-snap-type:none}
.hsc-pin .hsc-track{animation:hsc-slide linear both;animation-timeline:--hsc-pin;animation-range:contain 0% contain 100%}
.hsc-pin img{aspect-ratio:auto;height:min(46vh,calc(var(--hsc-card-w) * 1.25))}
.hsc-hint-pinned{display:block}
.hsc-pin .hsc-hint-swipe{display:none}
}
}
/* 2. snap gallery with buttons */
.hsc-controls{display:flex;gap:8px}
.hsc-controls[hidden]{display:none}
.hsc-controls button{display:grid;place-items:center;width:46px;height:46px;border:1px solid #d6d3d1;border-radius:50%;background:var(--hsc-card);color:var(--hsc-ink);font-size:1.1rem;cursor:pointer;transition:background-color .2s,border-color .2s}
.hsc-controls button:hover{border-color:var(--hsc-ink)}
.hsc-controls button:focus-visible{outline:3px solid var(--hsc-accent);outline-offset:2px}
.hsc-controls button[aria-disabled="true"]{color:#78716c;cursor:default;border-color:#e7e5e4}
@media (prefers-reduced-motion:reduce){.hsc-controls button{transition:none}}
JavaScript
// Previous/next buttons for scroll-snap galleries. The gallery works without this: it is a normal horizontal
// scroller that takes swipes, trackpads and arrow keys once focused.
(() => {
document.querySelectorAll('[data-hsc-gallery]').forEach(gallery => {
const scroller = gallery.querySelector('[data-hsc-scroller]');
const prev = gallery.querySelector('[data-hsc-prev]'), next = gallery.querySelector('[data-hsc-next]');
if (!scroller || !prev || !next) return;
prev.parentElement.hidden = false;
const step = () => scroller.clientWidth * 0.85;
const update = () => {
const max = scroller.scrollWidth - scroller.clientWidth - 2;
prev.setAttribute('aria-disabled', scroller.scrollLeft <= 2);
next.setAttribute('aria-disabled', scroller.scrollLeft >= max);
};
prev.addEventListener('click', () => scroller.scrollBy({ left: -step() }));
next.addEventListener('click', () => scroller.scrollBy({ left: step() }));
scroller.addEventListener('scroll', update, { passive: true });
addEventListener('resize', update);
update();
});
})();
Bootstrap 5 code
Requires: Bootstrap 5.3.8 CSS
HTML
<div><span class="lbl">Vertical scroll drives a horizontal track</span>
<section class="hsc hsc-pin" style="--hsc-count:7" aria-labelledby="hsc-pin-title">
<div class="hsc-pin-frame">
<div class="hsc-head d-flex flex-wrap align-items-end justify-content-between gap-3 mb-4">
<div><h2 id="hsc-pin-title">Selected work, 2021 to 2026</h2><p>Seven buildings by Studio Okapi, from a library in Bergen to a timber house in Ghent.</p></div>
<p class="hsc-hint hsc-hint-pinned mb-0">Scroll down to move along</p><p class="hsc-hint hsc-hint-swipe mb-0">Scroll sideways or use the arrow keys</p>
</div>
<div class="hsc-viewport" tabindex="0" role="region" aria-label="Selected work, scrolls sideways">
<ol class="hsc-track mb-0">
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1487958449943-2429e8be8625?auto=format&fit=crop&w=720&q=75" alt="White angular building with a glass base under a pale sky" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Bergen · 2025</small><b>Kvarteret Library</b><span>Public library, 6,200 m²</span></figcaption></figure></li>
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1479839672679-a46483c0e7c8?auto=format&fit=crop&w=720&q=75" alt="White apartment block with stacked, cantilevered balconies" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Lisbon · 2024</small><b>Alcântara Terraces</b><span>48 apartments</span></figcaption></figure></li>
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1518005020951-eccb494ad742?auto=format&fit=crop&w=720&q=75" alt="Curved white and blue facade seen from below" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Rotterdam · 2023</small><b>Maashaven Offices</b><span>Offices and a public roof</span></figcaption></figure></li>
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1524230572899-a752b3835840?auto=format&fit=crop&w=720&q=75" alt="A long white corridor of repeating arches" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Seville · 2023</small><b>Cloister Gallery</b><span>Museum extension</span></figcaption></figure></li>
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&fit=crop&w=720&q=75" alt="White villa with a pool and palm trees" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Mallorca · 2022</small><b>Casa Sóller</b><span>Private house</span></figcaption></figure></li>
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1496307653780-42ee777d4833?auto=format&fit=crop&w=720&q=75" alt="The tip of a white glass tower fading into a white sky" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Frankfurt · 2022</small><b>Linden Tower</b><span>Offices, 31 floors</span></figcaption></figure></li>
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=720&q=75" alt="Timber-clad family house with a deep porch and black side wing" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Ghent · 2021</small><b>Weaver's House</b><span>Private house</span></figcaption></figure></li>
</ol>
</div>
</div>
</section></div>
<div><span class="lbl">Scroll-snap gallery with buttons</span>
<section class="hsc" aria-labelledby="hsc-gal-title" data-hsc-gallery>
<div class="hsc-head d-flex flex-wrap align-items-end justify-content-between gap-3 mb-4">
<div><h2 id="hsc-gal-title">Interiors</h2><p>Rooms from recent projects. Swipe, scroll sideways or use the buttons.</p></div>
<div class="hsc-controls d-flex gap-2" hidden><button type="button" class="btn rounded-circle" data-hsc-prev aria-label="Previous interiors">←</button><button type="button" class="btn rounded-circle" data-hsc-next aria-label="Next interiors">→</button></div>
</div>
<div class="hsc-viewport" tabindex="0" role="region" aria-label="Interiors, scrolls sideways" data-hsc-scroller>
<ul class="hsc-track mb-0">
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=720&q=75" alt="Open living room with a white sofa, a timber wall and a kitchen beyond" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Living</small><b>Casa Sóller</b><span>Oak, lime plaster, terrazzo</span></figcaption></figure></li>
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1497366811353-6870744d04b2?auto=format&fit=crop&w=720&q=75" alt="Loft office with concrete ceiling, steel windows and a long table" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Workplace</small><b>Maashaven Offices</b><span>Exposed concrete, steel glazing</span></figcaption></figure></li>
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=720&q=75" alt="Long bright corridor with a dark kitchen wall and shelving" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Kitchen</small><b>Linden Tower, floor 31</b><span>Shared kitchen and lounge</span></figcaption></figure></li>
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=720&q=75" alt="Modern house at dusk with glowing floor-to-ceiling windows" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Evening</small><b>House in the Pines</b><span>Glass pavilion and terrace</span></figcaption></figure></li>
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1545324418-cc1a3fa10c00?auto=format&fit=crop&w=720&q=75" alt="Dark brick apartment building with balconies against a blue sky" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Housing</small><b>Nørrebro Yards</b><span>Brick, 64 homes</span></figcaption></figure></li>
<li class="hsc-card card"><figure class="mb-0"><img src="https://images.unsplash.com/photo-1431576901776-e539bd916ba2?auto=format&fit=crop&w=720&q=75" alt="Two gold glass towers seen from street level looking up" width="720" height="900" loading="lazy"><figcaption class="card-body"><small>Facade study</small><b>Gold Street Towers</b><span>Unbuilt competition entry</span></figcaption></figure></li>
</ul>
</div>
</section></div>
CSS
/* Horizontal scroll sections in Bootstrap 5: .card, .btn, .d-flex and .gap utilities for the parts; the
scrolling is the shared CSS below. By default each track is an ordinary horizontal scroll container with
scroll-snap, focusable so arrow keys scroll it. (1) Where scroll-driven animations are supported and motion is
allowed, the first section becomes tall, its frame sticks, and vertical page scrolling slides the track
sideways through a view() timeline. (2) The gallery stays a native snap scroller; a few lines of JavaScript
add previous/next buttons. */
.hsc{--hsc-ink:#171614;--hsc-muted:#57534e;--hsc-accent:#9a3412;--hsc-card:#fff;--hsc-radius:18px;--hsc-card-w:clamp(250px,30vw,360px);color:var(--hsc-ink)}
.hsc :where(h2,h3,p,ol,ul,figure){margin:0}
.hsc img{display:block;width:100%;height:auto;aspect-ratio:4/5;object-fit:cover}
.hsc-head h2{font-weight:700;font-size:clamp(1.6rem,1.2rem + 1.8vw,2.4rem);line-height:1.1;letter-spacing:-.03em}
.hsc-head p{margin-top:6px;max-width:48ch;color:var(--hsc-muted)}
.hsc-track{list-style:none;padding:0;display:flex;gap:18px;width:max-content}
.hsc-card{--bs-card-border-width:0;--bs-card-border-radius:var(--hsc-radius);width:var(--hsc-card-w);flex:none;scroll-snap-align:start;overflow:hidden;box-shadow:0 20px 40px -32px rgba(23,22,20,.55)}
.hsc-card figcaption{--bs-card-spacer-x:18px;--bs-card-spacer-y:14px;padding-bottom:18px}
.hsc-card b{display:block;font-size:1.02rem;font-weight:700}
.hsc-card span{font-size:.86rem;color:var(--hsc-muted)}
.hsc-card small{display:block;margin-bottom:2px;font-size:.72rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--hsc-accent)}
/* the scroller: native horizontal scrolling with snap points */
.hsc-viewport{overflow-x:auto;overscroll-behavior-x:contain;scroll-snap-type:x mandatory;scroll-padding-inline:4px;padding:4px 4px 18px;border-radius:var(--hsc-radius)}
.hsc-viewport:focus-visible{outline:3px solid var(--hsc-accent);outline-offset:3px}
@media (prefers-reduced-motion:no-preference){.hsc-viewport{scroll-behavior:smooth}}
/* 1. pinned section: vertical scroll drives the track */
.hsc-pin-frame{display:flex;flex-direction:column;justify-content:center}
.hsc-hint{font-size:.86rem;color:var(--hsc-muted)}
.hsc-hint-pinned{display:none}
@keyframes hsc-slide{to{transform:translateX(calc(-100% + 100cqi))}}
@supports (animation-timeline:view()){
@media (prefers-reduced-motion:no-preference){
.hsc-pin{height:calc(100vh + var(--hsc-count,6) * 0.45 * var(--hsc-card-w));view-timeline:--hsc-pin block}
.hsc-pin-frame{position:sticky;top:0;height:100vh;overflow:clip}
.hsc-pin .hsc-viewport{overflow:clip;container-type:inline-size;scroll-snap-type:none}
.hsc-pin .hsc-track{animation:hsc-slide linear both;animation-timeline:--hsc-pin;animation-range:contain 0% contain 100%}
.hsc-pin img{aspect-ratio:auto;height:min(46vh,calc(var(--hsc-card-w) * 1.25))}
.hsc-hint-pinned{display:block}
.hsc-pin .hsc-hint-swipe{display:none}
}
}
/* 2. snap gallery with buttons */
.hsc-controls[hidden]{display:none!important}
.hsc-controls .btn{--bs-btn-color:var(--hsc-ink);--bs-btn-bg:var(--hsc-card);--bs-btn-border-color:#d6d3d1;--bs-btn-hover-color:var(--hsc-ink);--bs-btn-hover-bg:var(--hsc-card);--bs-btn-hover-border-color:var(--hsc-ink);--bs-btn-active-bg:#f5f5f4;--bs-btn-active-color:var(--hsc-ink);
display:grid;place-items:center;width:46px;height:46px;padding:0;font-size:1.1rem}
.hsc-controls .btn:focus-visible{outline:3px solid var(--hsc-accent);outline-offset:2px;box-shadow:none}
.hsc-controls .btn[aria-disabled="true"]{--bs-btn-color:#78716c;--bs-btn-hover-border-color:#e7e5e4;--bs-btn-border-color:#e7e5e4;cursor:default}
@media (prefers-reduced-motion:reduce){.hsc-controls .btn{transition:none}}
JavaScript
// Previous/next buttons for scroll-snap galleries. The gallery works without this: it is a normal horizontal
// scroller that takes swipes, trackpads and arrow keys once focused.
(() => {
document.querySelectorAll('[data-hsc-gallery]').forEach(gallery => {
const scroller = gallery.querySelector('[data-hsc-scroller]');
const prev = gallery.querySelector('[data-hsc-prev]'), next = gallery.querySelector('[data-hsc-next]');
if (!scroller || !prev || !next) return;
prev.parentElement.hidden = false;
const step = () => scroller.clientWidth * 0.85;
const update = () => {
const max = scroller.scrollWidth - scroller.clientWidth - 2;
prev.setAttribute('aria-disabled', scroller.scrollLeft <= 2);
next.setAttribute('aria-disabled', scroller.scrollLeft >= max);
};
prev.addEventListener('click', () => scroller.scrollBy({ left: -step() }));
next.addEventListener('click', () => scroller.scrollBy({ left: step() }));
scroller.addEventListener('scroll', update, { passive: true });
addEventListener('resize', update);
update();
});
})();
Tailwind 4 code
Requires: Tailwind CSS 4
HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#57534e] uppercase">Vertical scroll drives a horizontal track</span>
<section class="text-[#171614] [--hsc-card-w:clamp(250px,30vw,360px)] [--hsc-count:7]" data-hsc-pin aria-labelledby="hsc-pin-title">
<div class="flex flex-col justify-center" data-hsc-frame>
<div class="mb-[22px] flex flex-wrap items-end justify-between gap-4">
<div><h2 class="text-[clamp(1.6rem,1.2rem+1.8vw,2.4rem)] leading-[1.1] font-bold tracking-[-.03em]" id="hsc-pin-title">Selected work, 2021 to 2026</h2><p class="mt-1.5 max-w-[48ch] text-[#57534e]">Seven buildings by Studio Okapi, from a library in Bergen to a timber house in Ghent.</p></div>
<p class="hidden text-[.86rem] text-[#57534e]" data-hsc-hint="pinned">Scroll down to move along</p><p class="text-[.86rem] text-[#57534e]" data-hsc-hint="swipe">Scroll sideways or use the arrow keys</p>
</div>
<div class="overflow-x-auto overscroll-x-contain snap-x snap-mandatory scroll-px-1 rounded-[18px] px-1 pt-1 pb-[18px] motion-safe:scroll-smooth focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#9a3412]" data-hsc-viewport tabindex="0" role="region" aria-label="Selected work, scrolls sideways">
<ol class="flex w-max gap-[18px]" data-hsc-track>
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1487958449943-2429e8be8625?auto=format&fit=crop&w=720&q=75" alt="White angular building with a glass base under a pale sky" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Bergen · 2025</small><b class="block text-[1.02rem]">Kvarteret Library</b><span class="text-[.86rem] text-[#57534e]">Public library, 6,200 m²</span></figcaption></figure></li>
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1479839672679-a46483c0e7c8?auto=format&fit=crop&w=720&q=75" alt="White apartment block with stacked, cantilevered balconies" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Lisbon · 2024</small><b class="block text-[1.02rem]">Alcântara Terraces</b><span class="text-[.86rem] text-[#57534e]">48 apartments</span></figcaption></figure></li>
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1518005020951-eccb494ad742?auto=format&fit=crop&w=720&q=75" alt="Curved white and blue facade seen from below" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Rotterdam · 2023</small><b class="block text-[1.02rem]">Maashaven Offices</b><span class="text-[.86rem] text-[#57534e]">Offices and a public roof</span></figcaption></figure></li>
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1524230572899-a752b3835840?auto=format&fit=crop&w=720&q=75" alt="A long white corridor of repeating arches" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Seville · 2023</small><b class="block text-[1.02rem]">Cloister Gallery</b><span class="text-[.86rem] text-[#57534e]">Museum extension</span></figcaption></figure></li>
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1512917774080-9991f1c4c750?auto=format&fit=crop&w=720&q=75" alt="White villa with a pool and palm trees" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Mallorca · 2022</small><b class="block text-[1.02rem]">Casa Sóller</b><span class="text-[.86rem] text-[#57534e]">Private house</span></figcaption></figure></li>
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1496307653780-42ee777d4833?auto=format&fit=crop&w=720&q=75" alt="The tip of a white glass tower fading into a white sky" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Frankfurt · 2022</small><b class="block text-[1.02rem]">Linden Tower</b><span class="text-[.86rem] text-[#57534e]">Offices, 31 floors</span></figcaption></figure></li>
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=720&q=75" alt="Timber-clad family house with a deep porch and black side wing" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Ghent · 2021</small><b class="block text-[1.02rem]">Weaver's House</b><span class="text-[.86rem] text-[#57534e]">Private house</span></figcaption></figure></li>
</ol>
</div>
</div>
</section></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#57534e] uppercase">Scroll-snap gallery with buttons</span>
<section class="text-[#171614] [--hsc-card-w:clamp(250px,30vw,360px)]" aria-labelledby="hsc-gal-title" data-hsc-gallery>
<div class="mb-[22px] flex flex-wrap items-end justify-between gap-4">
<div><h2 class="text-[clamp(1.6rem,1.2rem+1.8vw,2.4rem)] leading-[1.1] font-bold tracking-[-.03em]" id="hsc-gal-title">Interiors</h2><p class="mt-1.5 max-w-[48ch] text-[#57534e]">Rooms from recent projects. Swipe, scroll sideways or use the buttons.</p></div>
<div class="flex gap-2" hidden><button type="button" class="grid size-[46px] cursor-pointer place-items-center rounded-full border border-[#d6d3d1] bg-white text-[1.1rem] transition-colors duration-200 hover:border-[#171614] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#9a3412] aria-disabled:cursor-default aria-disabled:border-[#e7e5e4] aria-disabled:text-[#78716c] motion-reduce:transition-none" data-hsc-prev aria-label="Previous interiors">←</button><button type="button" class="grid size-[46px] cursor-pointer place-items-center rounded-full border border-[#d6d3d1] bg-white text-[1.1rem] transition-colors duration-200 hover:border-[#171614] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#9a3412] aria-disabled:cursor-default aria-disabled:border-[#e7e5e4] aria-disabled:text-[#78716c] motion-reduce:transition-none" data-hsc-next aria-label="Next interiors">→</button></div>
</div>
<div class="overflow-x-auto overscroll-x-contain snap-x snap-mandatory scroll-px-1 rounded-[18px] px-1 pt-1 pb-[18px] motion-safe:scroll-smooth focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#9a3412]" tabindex="0" role="region" aria-label="Interiors, scrolls sideways" data-hsc-scroller>
<ul class="flex w-max gap-[18px]">
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=720&q=75" alt="Open living room with a white sofa, a timber wall and a kitchen beyond" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Living</small><b class="block text-[1.02rem]">Casa Sóller</b><span class="text-[.86rem] text-[#57534e]">Oak, lime plaster, terrazzo</span></figcaption></figure></li>
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1497366811353-6870744d04b2?auto=format&fit=crop&w=720&q=75" alt="Loft office with concrete ceiling, steel windows and a long table" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Workplace</small><b class="block text-[1.02rem]">Maashaven Offices</b><span class="text-[.86rem] text-[#57534e]">Exposed concrete, steel glazing</span></figcaption></figure></li>
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=720&q=75" alt="Long bright corridor with a dark kitchen wall and shelving" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Kitchen</small><b class="block text-[1.02rem]">Linden Tower, floor 31</b><span class="text-[.86rem] text-[#57534e]">Shared kitchen and lounge</span></figcaption></figure></li>
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=720&q=75" alt="Modern house at dusk with glowing floor-to-ceiling windows" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Evening</small><b class="block text-[1.02rem]">House in the Pines</b><span class="text-[.86rem] text-[#57534e]">Glass pavilion and terrace</span></figcaption></figure></li>
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1545324418-cc1a3fa10c00?auto=format&fit=crop&w=720&q=75" alt="Dark brick apartment building with balconies against a blue sky" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Housing</small><b class="block text-[1.02rem]">Nørrebro Yards</b><span class="text-[.86rem] text-[#57534e]">Brick, 64 homes</span></figcaption></figure></li>
<li class="w-[var(--hsc-card-w)] flex-none snap-start overflow-hidden rounded-[18px] bg-white shadow-[0_20px_40px_-32px_rgba(23,22,20,.55)]"><figure><img class="block aspect-[4/5] h-auto w-full object-cover" data-hsc-img src="https://images.unsplash.com/photo-1431576901776-e539bd916ba2?auto=format&fit=crop&w=720&q=75" alt="Two gold glass towers seen from street level looking up" width="720" height="900" loading="lazy"><figcaption class="px-[18px] pt-3.5 pb-[18px]"><small class="mb-0.5 block text-[.72rem] font-bold tracking-[.12em] text-[#9a3412] uppercase">Facade study</small><b class="block text-[1.02rem]">Gold Street Towers</b><span class="text-[.86rem] text-[#57534e]">Unbuilt competition entry</span></figcaption></figure></li>
</ul>
</div>
</section></div>
CSS
/* Each track is a native snap scroller built from utilities (snap-x, snap-start, overflow-x-auto). The shared CSS
below pins the first section and lets vertical scrolling slide its track, only where view() timelines are
supported and motion is allowed; everywhere else it stays a sideways scroller. */
@keyframes hsc-slide{to{transform:translateX(calc(-100% + 100cqi))}}
@supports (animation-timeline:view()){
@media (prefers-reduced-motion:no-preference){
[data-hsc-pin]{height:calc(100vh + var(--hsc-count,6) * 0.45 * var(--hsc-card-w));view-timeline:--hsc-pin block}
[data-hsc-pin] [data-hsc-frame]{position:sticky;top:0;height:100vh;overflow:clip}
[data-hsc-pin] [data-hsc-viewport]{overflow:clip;container-type:inline-size;scroll-snap-type:none}
[data-hsc-pin] [data-hsc-track]{animation:hsc-slide linear both;animation-timeline:--hsc-pin;animation-range:contain 0% contain 100%}
[data-hsc-pin] [data-hsc-img]{aspect-ratio:auto;height:min(46vh,calc(var(--hsc-card-w) * 1.25))}
[data-hsc-pin] [data-hsc-hint="pinned"]{display:block}
[data-hsc-pin] [data-hsc-hint="swipe"]{display:none}
}
}
JavaScript
// Previous/next buttons for scroll-snap galleries. The gallery works without this: it is a normal horizontal
// scroller that takes swipes, trackpads and arrow keys once focused.
(() => {
document.querySelectorAll('[data-hsc-gallery]').forEach(gallery => {
const scroller = gallery.querySelector('[data-hsc-scroller]');
const prev = gallery.querySelector('[data-hsc-prev]'), next = gallery.querySelector('[data-hsc-next]');
if (!scroller || !prev || !next) return;
prev.parentElement.hidden = false;
const step = () => scroller.clientWidth * 0.85;
const update = () => {
const max = scroller.scrollWidth - scroller.clientWidth - 2;
prev.setAttribute('aria-disabled', scroller.scrollLeft <= 2);
next.setAttribute('aria-disabled', scroller.scrollLeft >= max);
};
prev.addEventListener('click', () => scroller.scrollBy({ left: -step() }));
next.addEventListener('click', () => scroller.scrollBy({ left: step() }));
scroller.addEventListener('scroll', update, { passive: true });
addEventListener('resize', update);
update();
});
})();
About this set
Two horizontal scroll sections for an architecture studio portfolio. The first pins a row of seven project cards and turns vertical page scrolling into sideways movement: the section becomes tall, its frame is position: sticky, and a view() timeline on the section moves the track from its first card to its last with a translateX that ends at -100% plus the container width, measured with container query units. When the section has scrolled past, the page carries on as normal. The second is an interiors gallery that stays a native horizontal scroller with mandatory scroll-snap, swipe and trackpad support, arrow-key scrolling once focused, and previous and next buttons that a few lines of JavaScript add and keep in step with the scroll position. Both start life as ordinary snap scrollers. The pinned behaviour is only switched on where scroll-driven animations are supported and motion is allowed, so in other browsers and under reduced motion the first section is simply a sideways scroller too, with a hint that says so.
What’s included
Pinned section where vertical scroll drives a horizontal track
Native scroll-snap gallery with previous and next buttons
Both rows fall back to keyboard-scrollable snap scrollers
Container query units size the track travel
Buttons only appear once the script has run
Accessibility
Each track sits in a labelled region with tabindex="0", a visible focus outline and arrow-key scrolling. The previous and next buttons have accessible names and report aria-disabled at either end. Cards are figures with alt text and a caption. Under reduced motion the pinned section becomes a normal sideways scroller and smooth scrolling is turned off.
Customise it
Change --hsc-card-w for the card width and --hsc-count to match the number of cards, which sets how much scrolling the pinned section takes. Colours are --hsc-ink, --hsc-muted and --hsc-accent; in Tailwind they are the arbitrary colour classes in the markup.