16 CSS Portfolio Layouts06 / 16
Horizontal Scrolling Portfolio Layout CSS Only
A side-scrolling showcase the way agencies build them: grid-auto-flow:column lays the panels in a row, scroll-snap lands each one crisply, and a scroll-driven progress bar (pure CSS animation-timeline: scroll()) fills as you traverse — zero JavaScript anywhere. The scroller is keyboard-operable out of the box (tabindex + arrow keys), momentum-friendly on trackpads, and each panel numbers itself with a CSS counter.
Published
The code
<section class="cpl-06" aria-label="Horizontal scrolling portfolio demo">
<div class="cpl-06__stage">
<header class="cpl-06__head">
<h2>Recent Work</h2>
<p>Scroll sideways — trackpad, arrows or drag</p>
</header>
<div class="cpl-06__rail" tabindex="0" role="region" aria-label="Projects, horizontally scrollable">
<a class="cpl-06__panel" href="#solace" style="--img:url('https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?q=80&w=900&auto=format&fit=crop');--a:oklch(0.76 0.12 280);--b:oklch(0.45 0.15 250)">
<figure></figure><div><h3>Solace Sleep</h3><p>DTC brand & storefront</p></div>
</a>
<a class="cpl-06__panel" href="#granite" style="--img:url('https://images.unsplash.com/photo-1581094794329-c8112a89af12?q=80&w=900&auto=format&fit=crop');--a:oklch(0.8 0.1 50);--b:oklch(0.52 0.14 30)">
<figure></figure><div><h3>Granite Supply</h3><p>B2B ordering platform</p></div>
</a>
<a class="cpl-06__panel" href="#murmur" style="--img:url('https://images.unsplash.com/photo-1505740420928-5e560c06d30e?q=80&w=900&auto=format&fit=crop');--a:oklch(0.78 0.11 330);--b:oklch(0.48 0.15 310)">
<figure></figure><div><h3>Murmur Audio</h3><p>Product launch site</p></div>
</a>
<a class="cpl-06__panel" href="#alpine" style="--img:url('https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=900&auto=format&fit=crop');--a:oklch(0.82 0.09 150);--b:oklch(0.52 0.12 180)">
<figure></figure><div><h3>Alpine Journal</h3><p>Editorial platform</p></div>
</a>
<a class="cpl-06__panel" href="#quartz" style="--img:url('https://images.unsplash.com/photo-1522542550221-31fd19575a2d?q=80&w=900&auto=format&fit=crop');--a:oklch(0.84 0.07 220);--b:oklch(0.55 0.11 250)">
<figure></figure><div><h3>Quartz Studio</h3><p>Portfolio & CMS</p></div>
</a>
</div>
<div class="cpl-06__track" aria-hidden="true"><i class="cpl-06__bar"></i></div>
</div>
</section><section class="cpl-06" aria-label="Horizontal scrolling portfolio demo">
<div class="cpl-06__stage">
<header class="cpl-06__head">
<h2>Recent Work</h2>
<p>Scroll sideways — trackpad, arrows or drag</p>
</header>
<div class="cpl-06__rail" tabindex="0" role="region" aria-label="Projects, horizontally scrollable">
<a class="cpl-06__panel" href="#solace" style="--img:url('https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?q=80&w=900&auto=format&fit=crop');--a:oklch(0.76 0.12 280);--b:oklch(0.45 0.15 250)">
<figure></figure><div><h3>Solace Sleep</h3><p>DTC brand & storefront</p></div>
</a>
<a class="cpl-06__panel" href="#granite" style="--img:url('https://images.unsplash.com/photo-1581094794329-c8112a89af12?q=80&w=900&auto=format&fit=crop');--a:oklch(0.8 0.1 50);--b:oklch(0.52 0.14 30)">
<figure></figure><div><h3>Granite Supply</h3><p>B2B ordering platform</p></div>
</a>
<a class="cpl-06__panel" href="#murmur" style="--img:url('https://images.unsplash.com/photo-1505740420928-5e560c06d30e?q=80&w=900&auto=format&fit=crop');--a:oklch(0.78 0.11 330);--b:oklch(0.48 0.15 310)">
<figure></figure><div><h3>Murmur Audio</h3><p>Product launch site</p></div>
</a>
<a class="cpl-06__panel" href="#alpine" style="--img:url('https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=900&auto=format&fit=crop');--a:oklch(0.82 0.09 150);--b:oklch(0.52 0.12 180)">
<figure></figure><div><h3>Alpine Journal</h3><p>Editorial platform</p></div>
</a>
<a class="cpl-06__panel" href="#quartz" style="--img:url('https://images.unsplash.com/photo-1522542550221-31fd19575a2d?q=80&w=900&auto=format&fit=crop');--a:oklch(0.84 0.07 220);--b:oklch(0.55 0.11 250)">
<figure></figure><div><h3>Quartz Studio</h3><p>Portfolio & CMS</p></div>
</a>
</div>
<div class="cpl-06__track" aria-hidden="true"><i class="cpl-06__bar"></i></div>
</div>
</section>.cpl-06,
.cpl-06 *,
.cpl-06 *::before,
.cpl-06 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cpl-06 {
--bg: oklch(0.21 0.02 275);
--ink: oklch(0.95 0.005 90);
--mut: oklch(0.66 0.015 275);
--line: oklch(0.31 0.02 275);
--acc: oklch(0.78 0.15 75);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
}
.cpl-06__stage {
width: 100%;
background: var(--bg);
padding: clamp(18px,4cqi,28px) 0;
timeline-scope: --cpl06;
min-height: 100vh;
min-height: 100svh;
display: flex;
flex-direction: column;
justify-content: center;
}
.cpl-06__head {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 14px;
padding: 0 24px;
margin-bottom: 18px;
}
.cpl-06__head h2 {
font-size: 24px;
letter-spacing: -.02em;
}
.cpl-06__head p {
font-size: 12.5px;
color: var(--mut);
}
.cpl-06__rail {
display: grid;
grid-auto-flow: column;
grid-auto-columns: min(520px,80%);
gap: 16px;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scroll-padding-inline: 24px;
padding: 4px 24px 18px;
counter-reset: cpl06;
scroll-timeline: --cpl06 x;
scrollbar-width: thin;
scrollbar-color: oklch(0.45 0.02 275) transparent;
}
.cpl-06__rail:focus-visible {
outline: 2px solid var(--acc);
outline-offset: -2px;
border-radius: 12px;
}
.cpl-06__panel {
scroll-snap-align: start;
counter-increment: cpl06;
display: grid;
gap: 0;
border-radius: 16px;
overflow: hidden;
text-decoration: none;
color: inherit;
background: oklch(0.26 0.02 275);
border: 1px solid var(--line);
transition: translate .3s cubic-bezier(.2,.7,.2,1),border-color .3s;
}
.cpl-06__panel:hover,
.cpl-06__panel:focus-visible {
translate: 0 -4px;
border-color: oklch(0.45 0.04 75);
}
.cpl-06__panel:focus-visible {
outline: 2px solid var(--acc);
outline-offset: 3px;
}
.cpl-06__panel figure {
position: relative;
aspect-ratio: 16/10;
background-image: var(--img,none),linear-gradient(140deg,var(--a),var(--b));
background-size: cover;
background-position: center;
}
.cpl-06__panel figure::after {
content: "0" counter(cpl06);
position: absolute;
top: 12px;
left: 14px;
font-size: 12px;
font-weight: 800;
letter-spacing: .08em;
color: oklch(1 0 0/.85);
font-variant-numeric: tabular-nums;
}
.cpl-06__panel div {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
}
.cpl-06__panel h3 {
font-size: 15.5px;
letter-spacing: -.01em;
}
.cpl-06__panel p {
font-size: 12px;
color: var(--mut);
white-space: nowrap;
}
.cpl-06__track {
display: none;
}
@supports (animation-timeline: scroll()) {
.cpl-06__track {
display: block;
height: 3px;
margin: 14px 24px 0;
border-radius: 99px;
background: oklch(0.32 0.02 275);
}
.cpl-06__bar {
display: block;
height: 100%;
border-radius: 99px;
background: var(--acc);
transform-origin: left;
animation: cpl06-grow linear both;
animation-timeline: --cpl06;
}
@keyframes cpl06-grow {
from {
transform: scaleX(.06);
}
to {
transform: scaleX(1);
}
}
}
.cpl-06__panel figure::after {
text-shadow: 0 1px 10px oklch(0.1 0 0/.6);
}
@media (prefers-reduced-motion: reduce) {
.cpl-06__panel {
transition-duration: .01ms !important;
}
}.cpl-06,
.cpl-06 *,
.cpl-06 *::before,
.cpl-06 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cpl-06 {
--bg: oklch(0.21 0.02 275);
--ink: oklch(0.95 0.005 90);
--mut: oklch(0.66 0.015 275);
--line: oklch(0.31 0.02 275);
--acc: oklch(0.78 0.15 75);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
}
.cpl-06__stage {
width: 100%;
background: var(--bg);
padding: clamp(18px,4cqi,28px) 0;
timeline-scope: --cpl06;
min-height: 100vh;
min-height: 100svh;
display: flex;
flex-direction: column;
justify-content: center;
}
.cpl-06__head {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 14px;
padding: 0 24px;
margin-bottom: 18px;
}
.cpl-06__head h2 {
font-size: 24px;
letter-spacing: -.02em;
}
.cpl-06__head p {
font-size: 12.5px;
color: var(--mut);
}
.cpl-06__rail {
display: grid;
grid-auto-flow: column;
grid-auto-columns: min(520px,80%);
gap: 16px;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scroll-padding-inline: 24px;
padding: 4px 24px 18px;
counter-reset: cpl06;
scroll-timeline: --cpl06 x;
scrollbar-width: thin;
scrollbar-color: oklch(0.45 0.02 275) transparent;
}
.cpl-06__rail:focus-visible {
outline: 2px solid var(--acc);
outline-offset: -2px;
border-radius: 12px;
}
.cpl-06__panel {
scroll-snap-align: start;
counter-increment: cpl06;
display: grid;
gap: 0;
border-radius: 16px;
overflow: hidden;
text-decoration: none;
color: inherit;
background: oklch(0.26 0.02 275);
border: 1px solid var(--line);
transition: translate .3s cubic-bezier(.2,.7,.2,1),border-color .3s;
}
.cpl-06__panel:hover,
.cpl-06__panel:focus-visible {
translate: 0 -4px;
border-color: oklch(0.45 0.04 75);
}
.cpl-06__panel:focus-visible {
outline: 2px solid var(--acc);
outline-offset: 3px;
}
.cpl-06__panel figure {
position: relative;
aspect-ratio: 16/10;
background-image: var(--img,none),linear-gradient(140deg,var(--a),var(--b));
background-size: cover;
background-position: center;
}
.cpl-06__panel figure::after {
content: "0" counter(cpl06);
position: absolute;
top: 12px;
left: 14px;
font-size: 12px;
font-weight: 800;
letter-spacing: .08em;
color: oklch(1 0 0/.85);
font-variant-numeric: tabular-nums;
}
.cpl-06__panel div {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
}
.cpl-06__panel h3 {
font-size: 15.5px;
letter-spacing: -.01em;
}
.cpl-06__panel p {
font-size: 12px;
color: var(--mut);
white-space: nowrap;
}
.cpl-06__track {
display: none;
}
@supports (animation-timeline: scroll()) {
.cpl-06__track {
display: block;
height: 3px;
margin: 14px 24px 0;
border-radius: 99px;
background: oklch(0.32 0.02 275);
}
.cpl-06__bar {
display: block;
height: 100%;
border-radius: 99px;
background: var(--acc);
transform-origin: left;
animation: cpl06-grow linear both;
animation-timeline: --cpl06;
}
@keyframes cpl06-grow {
from {
transform: scaleX(.06);
}
to {
transform: scaleX(1);
}
}
}
.cpl-06__panel figure::after {
text-shadow: 0 1px 10px oklch(0.1 0 0/.6);
}
@media (prefers-reduced-motion: reduce) {
.cpl-06__panel {
transition-duration: .01ms !important;
}
}Here's a working CSS Portfolio Layout from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Horizontal Scrolling Portfolio Layout CSS Only
Source: https://codefronts.com/layouts/css-portfolio-layouts/horizontal-scrolling-portfolio-layout-css-only/
A side-scrolling showcase the way agencies build them: grid-auto-flow:column lays the panels in a row, scroll-snap lands each one crisply, and a scroll-driven progress bar (pure CSS animation-timeline: scroll()) fills as you traverse — zero JavaScript anywhere. The scroller is keyboard-operable out of the box (tabindex + arrow keys), momentum-friendly on trackpads, and each panel numbers itself with a CSS counter.
## HTML
```html
<section class="cpl-06" aria-label="Horizontal scrolling portfolio demo">
<div class="cpl-06__stage">
<header class="cpl-06__head">
<h2>Recent Work</h2>
<p>Scroll sideways — trackpad, arrows or drag</p>
</header>
<div class="cpl-06__rail" tabindex="0" role="region" aria-label="Projects, horizontally scrollable">
<a class="cpl-06__panel" href="#solace" style="--img:url('https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?q=80&w=900&auto=format&fit=crop');--a:oklch(0.76 0.12 280);--b:oklch(0.45 0.15 250)">
<figure></figure><div><h3>Solace Sleep</h3><p>DTC brand & storefront</p></div>
</a>
<a class="cpl-06__panel" href="#granite" style="--img:url('https://images.unsplash.com/photo-1581094794329-c8112a89af12?q=80&w=900&auto=format&fit=crop');--a:oklch(0.8 0.1 50);--b:oklch(0.52 0.14 30)">
<figure></figure><div><h3>Granite Supply</h3><p>B2B ordering platform</p></div>
</a>
<a class="cpl-06__panel" href="#murmur" style="--img:url('https://images.unsplash.com/photo-1505740420928-5e560c06d30e?q=80&w=900&auto=format&fit=crop');--a:oklch(0.78 0.11 330);--b:oklch(0.48 0.15 310)">
<figure></figure><div><h3>Murmur Audio</h3><p>Product launch site</p></div>
</a>
<a class="cpl-06__panel" href="#alpine" style="--img:url('https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=900&auto=format&fit=crop');--a:oklch(0.82 0.09 150);--b:oklch(0.52 0.12 180)">
<figure></figure><div><h3>Alpine Journal</h3><p>Editorial platform</p></div>
</a>
<a class="cpl-06__panel" href="#quartz" style="--img:url('https://images.unsplash.com/photo-1522542550221-31fd19575a2d?q=80&w=900&auto=format&fit=crop');--a:oklch(0.84 0.07 220);--b:oklch(0.55 0.11 250)">
<figure></figure><div><h3>Quartz Studio</h3><p>Portfolio & CMS</p></div>
</a>
</div>
<div class="cpl-06__track" aria-hidden="true"><i class="cpl-06__bar"></i></div>
</div>
</section>
```
## CSS
```css
.cpl-06,
.cpl-06 *,
.cpl-06 *::before,
.cpl-06 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cpl-06 {
--bg: oklch(0.21 0.02 275);
--ink: oklch(0.95 0.005 90);
--mut: oklch(0.66 0.015 275);
--line: oklch(0.31 0.02 275);
--acc: oklch(0.78 0.15 75);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
}
.cpl-06__stage {
width: 100%;
background: var(--bg);
padding: clamp(18px,4cqi,28px) 0;
timeline-scope: --cpl06;
min-height: 100vh;
min-height: 100svh;
display: flex;
flex-direction: column;
justify-content: center;
}
.cpl-06__head {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 14px;
padding: 0 24px;
margin-bottom: 18px;
}
.cpl-06__head h2 {
font-size: 24px;
letter-spacing: -.02em;
}
.cpl-06__head p {
font-size: 12.5px;
color: var(--mut);
}
.cpl-06__rail {
display: grid;
grid-auto-flow: column;
grid-auto-columns: min(520px,80%);
gap: 16px;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scroll-padding-inline: 24px;
padding: 4px 24px 18px;
counter-reset: cpl06;
scroll-timeline: --cpl06 x;
scrollbar-width: thin;
scrollbar-color: oklch(0.45 0.02 275) transparent;
}
.cpl-06__rail:focus-visible {
outline: 2px solid var(--acc);
outline-offset: -2px;
border-radius: 12px;
}
.cpl-06__panel {
scroll-snap-align: start;
counter-increment: cpl06;
display: grid;
gap: 0;
border-radius: 16px;
overflow: hidden;
text-decoration: none;
color: inherit;
background: oklch(0.26 0.02 275);
border: 1px solid var(--line);
transition: translate .3s cubic-bezier(.2,.7,.2,1),border-color .3s;
}
.cpl-06__panel:hover,
.cpl-06__panel:focus-visible {
translate: 0 -4px;
border-color: oklch(0.45 0.04 75);
}
.cpl-06__panel:focus-visible {
outline: 2px solid var(--acc);
outline-offset: 3px;
}
.cpl-06__panel figure {
position: relative;
aspect-ratio: 16/10;
background-image: var(--img,none),linear-gradient(140deg,var(--a),var(--b));
background-size: cover;
background-position: center;
}
.cpl-06__panel figure::after {
content: "0" counter(cpl06);
position: absolute;
top: 12px;
left: 14px;
font-size: 12px;
font-weight: 800;
letter-spacing: .08em;
color: oklch(1 0 0/.85);
font-variant-numeric: tabular-nums;
}
.cpl-06__panel div {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
}
.cpl-06__panel h3 {
font-size: 15.5px;
letter-spacing: -.01em;
}
.cpl-06__panel p {
font-size: 12px;
color: var(--mut);
white-space: nowrap;
}
.cpl-06__track {
display: none;
}
@supports (animation-timeline: scroll()) {
.cpl-06__track {
display: block;
height: 3px;
margin: 14px 24px 0;
border-radius: 99px;
background: oklch(0.32 0.02 275);
}
.cpl-06__bar {
display: block;
height: 100%;
border-radius: 99px;
background: var(--acc);
transform-origin: left;
animation: cpl06-grow linear both;
animation-timeline: --cpl06;
}
@keyframes cpl06-grow {
from {
transform: scaleX(.06);
}
to {
transform: scaleX(1);
}
}
}
.cpl-06__panel figure::after {
text-shadow: 0 1px 10px oklch(0.1 0 0/.6);
}
@media (prefers-reduced-motion: reduce) {
.cpl-06__panel {
transition-duration: .01ms !important;
}
}
```Here's a working CSS Portfolio Layout from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Horizontal Scrolling Portfolio Layout CSS Only
Source: https://codefronts.com/layouts/css-portfolio-layouts/horizontal-scrolling-portfolio-layout-css-only/
A side-scrolling showcase the way agencies build them: grid-auto-flow:column lays the panels in a row, scroll-snap lands each one crisply, and a scroll-driven progress bar (pure CSS animation-timeline: scroll()) fills as you traverse — zero JavaScript anywhere. The scroller is keyboard-operable out of the box (tabindex + arrow keys), momentum-friendly on trackpads, and each panel numbers itself with a CSS counter.
## HTML
```html
<section class="cpl-06" aria-label="Horizontal scrolling portfolio demo">
<div class="cpl-06__stage">
<header class="cpl-06__head">
<h2>Recent Work</h2>
<p>Scroll sideways — trackpad, arrows or drag</p>
</header>
<div class="cpl-06__rail" tabindex="0" role="region" aria-label="Projects, horizontally scrollable">
<a class="cpl-06__panel" href="#solace" style="--img:url('https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?q=80&w=900&auto=format&fit=crop');--a:oklch(0.76 0.12 280);--b:oklch(0.45 0.15 250)">
<figure></figure><div><h3>Solace Sleep</h3><p>DTC brand & storefront</p></div>
</a>
<a class="cpl-06__panel" href="#granite" style="--img:url('https://images.unsplash.com/photo-1581094794329-c8112a89af12?q=80&w=900&auto=format&fit=crop');--a:oklch(0.8 0.1 50);--b:oklch(0.52 0.14 30)">
<figure></figure><div><h3>Granite Supply</h3><p>B2B ordering platform</p></div>
</a>
<a class="cpl-06__panel" href="#murmur" style="--img:url('https://images.unsplash.com/photo-1505740420928-5e560c06d30e?q=80&w=900&auto=format&fit=crop');--a:oklch(0.78 0.11 330);--b:oklch(0.48 0.15 310)">
<figure></figure><div><h3>Murmur Audio</h3><p>Product launch site</p></div>
</a>
<a class="cpl-06__panel" href="#alpine" style="--img:url('https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=900&auto=format&fit=crop');--a:oklch(0.82 0.09 150);--b:oklch(0.52 0.12 180)">
<figure></figure><div><h3>Alpine Journal</h3><p>Editorial platform</p></div>
</a>
<a class="cpl-06__panel" href="#quartz" style="--img:url('https://images.unsplash.com/photo-1522542550221-31fd19575a2d?q=80&w=900&auto=format&fit=crop');--a:oklch(0.84 0.07 220);--b:oklch(0.55 0.11 250)">
<figure></figure><div><h3>Quartz Studio</h3><p>Portfolio & CMS</p></div>
</a>
</div>
<div class="cpl-06__track" aria-hidden="true"><i class="cpl-06__bar"></i></div>
</div>
</section>
```
## CSS
```css
.cpl-06,
.cpl-06 *,
.cpl-06 *::before,
.cpl-06 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cpl-06 {
--bg: oklch(0.21 0.02 275);
--ink: oklch(0.95 0.005 90);
--mut: oklch(0.66 0.015 275);
--line: oklch(0.31 0.02 275);
--acc: oklch(0.78 0.15 75);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
}
.cpl-06__stage {
width: 100%;
background: var(--bg);
padding: clamp(18px,4cqi,28px) 0;
timeline-scope: --cpl06;
min-height: 100vh;
min-height: 100svh;
display: flex;
flex-direction: column;
justify-content: center;
}
.cpl-06__head {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 14px;
padding: 0 24px;
margin-bottom: 18px;
}
.cpl-06__head h2 {
font-size: 24px;
letter-spacing: -.02em;
}
.cpl-06__head p {
font-size: 12.5px;
color: var(--mut);
}
.cpl-06__rail {
display: grid;
grid-auto-flow: column;
grid-auto-columns: min(520px,80%);
gap: 16px;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scroll-padding-inline: 24px;
padding: 4px 24px 18px;
counter-reset: cpl06;
scroll-timeline: --cpl06 x;
scrollbar-width: thin;
scrollbar-color: oklch(0.45 0.02 275) transparent;
}
.cpl-06__rail:focus-visible {
outline: 2px solid var(--acc);
outline-offset: -2px;
border-radius: 12px;
}
.cpl-06__panel {
scroll-snap-align: start;
counter-increment: cpl06;
display: grid;
gap: 0;
border-radius: 16px;
overflow: hidden;
text-decoration: none;
color: inherit;
background: oklch(0.26 0.02 275);
border: 1px solid var(--line);
transition: translate .3s cubic-bezier(.2,.7,.2,1),border-color .3s;
}
.cpl-06__panel:hover,
.cpl-06__panel:focus-visible {
translate: 0 -4px;
border-color: oklch(0.45 0.04 75);
}
.cpl-06__panel:focus-visible {
outline: 2px solid var(--acc);
outline-offset: 3px;
}
.cpl-06__panel figure {
position: relative;
aspect-ratio: 16/10;
background-image: var(--img,none),linear-gradient(140deg,var(--a),var(--b));
background-size: cover;
background-position: center;
}
.cpl-06__panel figure::after {
content: "0" counter(cpl06);
position: absolute;
top: 12px;
left: 14px;
font-size: 12px;
font-weight: 800;
letter-spacing: .08em;
color: oklch(1 0 0/.85);
font-variant-numeric: tabular-nums;
}
.cpl-06__panel div {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
}
.cpl-06__panel h3 {
font-size: 15.5px;
letter-spacing: -.01em;
}
.cpl-06__panel p {
font-size: 12px;
color: var(--mut);
white-space: nowrap;
}
.cpl-06__track {
display: none;
}
@supports (animation-timeline: scroll()) {
.cpl-06__track {
display: block;
height: 3px;
margin: 14px 24px 0;
border-radius: 99px;
background: oklch(0.32 0.02 275);
}
.cpl-06__bar {
display: block;
height: 100%;
border-radius: 99px;
background: var(--acc);
transform-origin: left;
animation: cpl06-grow linear both;
animation-timeline: --cpl06;
}
@keyframes cpl06-grow {
from {
transform: scaleX(.06);
}
to {
transform: scaleX(1);
}
}
}
.cpl-06__panel figure::after {
text-shadow: 0 1px 10px oklch(0.1 0 0/.6);
}
@media (prefers-reduced-motion: reduce) {
.cpl-06__panel {
transition-duration: .01ms !important;
}
}
```How this works
Three parts. First the rail — a grid that flows sideways instead of wrapping, with snap points on every panel:
.rail{ display:grid; grid-auto-flow:column;
grid-auto-columns: min(420px, 78%);
gap:16px; overflow-x:auto;
scroll-snap-type: x mandatory;
scroll-padding-inline: 24px; }
.panel{ scroll-snap-align: start; }scroll-padding-inline matters more than it looks: it offsets every snap position by the rail's padding so the first and last panels can actually reach their snapped spot instead of hiding under the edge. grid-auto-columns: min(420px, 78%) keeps a sliver of the next panel visible at every width — the single strongest 'this scrolls sideways' affordance you can give users.
Second, the progress bar is a scroll-driven animation: the rail declares scroll-timeline: --cpl06 x, the stage broadcasts it with timeline-scope, and the bar plays a scaleX keyframe against that timeline instead of the clock — animation-timeline: --cpl06. No scroll listeners, runs off-main-thread, and the whole block sits inside @supports (animation-timeline: scroll()) so browsers without it simply show no bar. Third, keyboard: the rail carries tabindex='0' + a visible focus ring + an aria-label, making arrow-key scrolling work everywhere without a line of script.
Make it yours
- Panel width:
min(420px, 78%)— the 78% is the peek amount (lower % = more of the next panel showing); the 420px caps panel width on wide embeds. - Snap strictness:
mandatoryalways lands on a panel; switch toproximityif panels may be taller/wider than the viewport so users are never trapped mid-content. - Snap alignment:
startgives an editorial left-aligned rhythm;centermakes a carousel. Change it on the panel, not the rail. - Progress bar → dots: keep the same scroll-timeline and animate
background-positionof a repeating gradient for a stepped indicator, still zero JS.
Gotchas — read before shipping
- Horizontal scroll must never hijack vertical: this pattern scopes overflow-x to ONE rail inside a normal page — do not convert page-level vertical scroll to horizontal with JS; it breaks a11y, find-in-page and deep links.
- Without
tabindex='0'a scroll region is unreachable by keyboard in most browsers — and WCAG 2.1.1 fails. Add the label too: role and name make it a proper widget. scroll-snap-type: x mandatory+ panels wider than the scrollport = content you can never see; that's whatproximityis for.- Hiding scrollbars entirely (scrollbar-width:none) removes the only visual scroll signal on Windows — this demo styles a slim one instead of deleting it.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 115+ (progress bar) | 17+ core / 26+ progress bar | 128+ core | 115+ (progress bar) |
Rail, snap and keyboard work everywhere (scroll-snap is Baseline 2019). The scroll-driven progress bar is progressive enhancement inside @supports — Chrome/Edge 115+, and it simply doesn't render elsewhere.