16 CSS Portfolio Layouts16 / 16
Sticky Header Sidebar Portfolio Layout Template CSS
The full app-shell template: a sticky header across the top, a sticky section rail down the left, and a long project list scrolling between them — the modern 'holy grail' with grid-template-areas naming the regions and two cooperating stickies doing what fixed positioning used to fake. The only trick worth memorizing is the offset handshake: the sidebar sticks at top: var(--headerH), so the two never fight.
Published
The code
<section class="cpl-16" aria-label="Sticky header and sidebar portfolio template demo">
<div class="cpl-16__stage">
<div class="cpl-16__shell">
<header class="cpl-16__header">
<span class="cpl-16__brand"><i aria-hidden="true"></i>Archive of Work</span>
<a class="cpl-16__hire" href="#hire">Hire me</a>
</header>
<aside class="cpl-16__side">
<nav aria-label="Sections">
<a href="#cpl16-2026" aria-current="true">2026</a>
<a href="#cpl16-2025">2025</a>
<a href="#cpl16-2024">2024</a>
<a href="#cpl16-experiments">Experiments</a>
</nav>
<p class="cpl-16__count">23 projects · 2019—2026</p>
</aside>
<main class="cpl-16__main">
<h3 id="cpl16-2026">2026</h3>
<a class="cpl-16__item" href="#halcyon" style="--img:url('https://images.unsplash.com/photo-1481627834876-b7833e8f5570?q=80&w=900&auto=format&fit=crop');--a:oklch(0.78 0.11 280);--b:oklch(0.5 0.15 305)"><figure></figure><div><h4>Halcyon Reader</h4><p>RSS client & brand — calm software, calmer landing page.</p></div><span>Web</span></a>
<a class="cpl-16__item" href="#matterhorn" style="--img:url('https://images.unsplash.com/photo-1551632811-561732d1e306?q=80&w=900&auto=format&fit=crop');--a:oklch(0.82 0.09 70);--b:oklch(0.55 0.14 45)"><figure></figure><div><h4>Matterhorn Gear Co.</h4><p>Storefront rebuild — 2.1s LCP to 0.8s on the same catalogue.</p></div><span>E-comm</span></a>
<h3 id="cpl16-2025">2025</h3>
<a class="cpl-16__item" href="#quill" style="--img:url('https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=900&auto=format&fit=crop');--a:oklch(0.8 0.1 160);--b:oklch(0.52 0.12 185)"><figure></figure><div><h4>Quill Grants</h4><p>Application portal for a state arts fund — WCAG 2.2 AA end to end.</p></div><span>Gov</span></a>
<a class="cpl-16__item" href="#bellweather" style="--img:url('https://images.unsplash.com/photo-1478737270239-2f02b77fc618?q=80&w=900&auto=format&fit=crop');--a:oklch(0.76 0.12 20);--b:oklch(0.5 0.16 350)"><figure></figure><div><h4>Bellweather Radio</h4><p>Live player and 40-year episode archive, searchable in 90ms.</p></div><span>Media</span></a>
<h3 id="cpl16-2024">2024</h3>
<a class="cpl-16__item" href="#cartog" style="--img:url('https://images.unsplash.com/photo-1524661135-423995f22d0b?q=80&w=900&auto=format&fit=crop');--a:oklch(0.84 0.07 230);--b:oklch(0.56 0.11 260)"><figure></figure><div><h4>Cartog</h4><p>Map-annotation tool for field researchers; offline-first.</p></div><span>Tool</span></a>
<h3 id="cpl16-experiments">Experiments</h3>
<a class="cpl-16__item" href="#typetheremin" style="--img:url('https://images.unsplash.com/photo-1511512578047-dfb367046420?q=80&w=900&auto=format&fit=crop');--a:oklch(0.7 0.13 320);--b:oklch(0.45 0.14 345)"><figure></figure><div><h4>Type Theremin</h4><p>Variable-font weight played by cursor position. Useless, beloved.</p></div><span>Play</span></a>
</main>
</div>
</div>
</section><section class="cpl-16" aria-label="Sticky header and sidebar portfolio template demo">
<div class="cpl-16__stage">
<div class="cpl-16__shell">
<header class="cpl-16__header">
<span class="cpl-16__brand"><i aria-hidden="true"></i>Archive of Work</span>
<a class="cpl-16__hire" href="#hire">Hire me</a>
</header>
<aside class="cpl-16__side">
<nav aria-label="Sections">
<a href="#cpl16-2026" aria-current="true">2026</a>
<a href="#cpl16-2025">2025</a>
<a href="#cpl16-2024">2024</a>
<a href="#cpl16-experiments">Experiments</a>
</nav>
<p class="cpl-16__count">23 projects · 2019—2026</p>
</aside>
<main class="cpl-16__main">
<h3 id="cpl16-2026">2026</h3>
<a class="cpl-16__item" href="#halcyon" style="--img:url('https://images.unsplash.com/photo-1481627834876-b7833e8f5570?q=80&w=900&auto=format&fit=crop');--a:oklch(0.78 0.11 280);--b:oklch(0.5 0.15 305)"><figure></figure><div><h4>Halcyon Reader</h4><p>RSS client & brand — calm software, calmer landing page.</p></div><span>Web</span></a>
<a class="cpl-16__item" href="#matterhorn" style="--img:url('https://images.unsplash.com/photo-1551632811-561732d1e306?q=80&w=900&auto=format&fit=crop');--a:oklch(0.82 0.09 70);--b:oklch(0.55 0.14 45)"><figure></figure><div><h4>Matterhorn Gear Co.</h4><p>Storefront rebuild — 2.1s LCP to 0.8s on the same catalogue.</p></div><span>E-comm</span></a>
<h3 id="cpl16-2025">2025</h3>
<a class="cpl-16__item" href="#quill" style="--img:url('https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=900&auto=format&fit=crop');--a:oklch(0.8 0.1 160);--b:oklch(0.52 0.12 185)"><figure></figure><div><h4>Quill Grants</h4><p>Application portal for a state arts fund — WCAG 2.2 AA end to end.</p></div><span>Gov</span></a>
<a class="cpl-16__item" href="#bellweather" style="--img:url('https://images.unsplash.com/photo-1478737270239-2f02b77fc618?q=80&w=900&auto=format&fit=crop');--a:oklch(0.76 0.12 20);--b:oklch(0.5 0.16 350)"><figure></figure><div><h4>Bellweather Radio</h4><p>Live player and 40-year episode archive, searchable in 90ms.</p></div><span>Media</span></a>
<h3 id="cpl16-2024">2024</h3>
<a class="cpl-16__item" href="#cartog" style="--img:url('https://images.unsplash.com/photo-1524661135-423995f22d0b?q=80&w=900&auto=format&fit=crop');--a:oklch(0.84 0.07 230);--b:oklch(0.56 0.11 260)"><figure></figure><div><h4>Cartog</h4><p>Map-annotation tool for field researchers; offline-first.</p></div><span>Tool</span></a>
<h3 id="cpl16-experiments">Experiments</h3>
<a class="cpl-16__item" href="#typetheremin" style="--img:url('https://images.unsplash.com/photo-1511512578047-dfb367046420?q=80&w=900&auto=format&fit=crop');--a:oklch(0.7 0.13 320);--b:oklch(0.45 0.14 345)"><figure></figure><div><h4>Type Theremin</h4><p>Variable-font weight played by cursor position. Useless, beloved.</p></div><span>Play</span></a>
</main>
</div>
</div>
</section>.cpl-16,
.cpl-16 *,
.cpl-16 *::before,
.cpl-16 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cpl-16 {
--headerH: 58px;
--bg: oklch(0.975 0.005 100);
--ink: oklch(0.24 0.02 280);
--mut: oklch(0.54 0.015 280);
--line: oklch(0.89 0.008 280);
--acc: oklch(0.6 0.17 300);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
}
.cpl-16__stage {
width: 100%;
container: cpl16/inline-size;
height: 100vh;
height: 100svh;
overflow-y: auto;
background: var(--bg);
}
.cpl-16__shell {
display: grid;
min-height: 100%;
grid-template-columns: 200px 1fr;
grid-template-rows: var(--headerH) 1fr;
grid-template-areas: "header header" "side main";
}
.cpl-16__header {
grid-area: header;
position: sticky;
top: 0;
z-index: 3;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 22px;
background: color-mix(in oklch,var(--bg) 82%,transparent);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.cpl-16__brand {
display: flex;
align-items: center;
gap: 9px;
font-size: 14px;
font-weight: 800;
letter-spacing: -.01em;
}
.cpl-16__brand i {
width: 11px;
height: 11px;
border-radius: 50%;
background: conic-gradient(from 120deg,var(--acc),oklch(0.75 0.12 200),var(--acc));
}
.cpl-16__hire {
display: grid;
place-items: center;
min-height: 36px;
padding: 0 15px;
border-radius: 99px;
font-size: 12.5px;
font-weight: 700;
text-decoration: none;
color: oklch(0.99 0 0);
background: var(--ink);
transition: background .2s,translate .2s;
}
.cpl-16__hire:hover,
.cpl-16__hire:focus-visible {
background: var(--acc);
translate: 0 -1px;
}
.cpl-16__hire:focus-visible {
outline: 2px solid var(--acc);
outline-offset: 3px;
}
.cpl-16__side {
grid-area: side;
position: sticky;
top: var(--headerH);
align-self: start;
height: calc(100vh - var(--headerH));
height: calc(100svh - var(--headerH));
display: flex;
flex-direction: column;
gap: 14px;
padding: 22px 18px;
border-right: 1px solid var(--line);
}
.cpl-16__side nav {
display: grid;
gap: 2px;
}
.cpl-16__side nav a {
display: flex;
align-items: center;
min-height: 38px;
padding: 0 12px;
border-radius: 9px;
font-size: 13.5px;
font-weight: 600;
text-decoration: none;
color: var(--mut);
border-left: 2px solid transparent;
transition: color .2s,background .2s,border-color .2s;
}
.cpl-16__side nav a:hover,
.cpl-16__side nav a:focus-visible {
color: var(--ink);
background: oklch(0.93 0.01 100);
}
.cpl-16__side nav a[aria-current] {
color: var(--ink);
border-left-color: var(--acc);
background: oklch(0.94 0.015 300);
}
.cpl-16__side nav a:focus-visible {
outline: 2px solid var(--acc);
outline-offset: -2px;
}
.cpl-16__count {
margin-top: auto;
font-size: 11.5px;
color: var(--mut);
}
.cpl-16__main {
grid-area: main;
padding: 22px 24px 34px;
display: grid;
gap: 12px;
align-content: start;
}
.cpl-16__main h3 {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--mut);
margin-top: 14px;
scroll-margin-top: calc(var(--headerH) + 12px);
}
.cpl-16__main h3:first-child {
margin-top: 0;
}
.cpl-16__item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px;
margin: 0 -12px;
border-radius: 14px;
text-decoration: none;
color: inherit;
transition: background .2s;
}
.cpl-16__item:hover,
.cpl-16__item:focus-visible {
background: oklch(0.94 0.008 100);
}
.cpl-16__item:focus-visible {
outline: 2px solid var(--acc);
outline-offset: -2px;
}
.cpl-16__item figure {
flex: none;
width: 92px;
aspect-ratio: 4/3;
border-radius: 10px;
background-image: var(--img,none),linear-gradient(140deg,var(--a),var(--b));
background-size: cover;
background-position: center;
}
.cpl-16__item h4 {
font-size: 14.5px;
letter-spacing: -.01em;
}
.cpl-16__item p {
margin-top: 3px;
font-size: 12.5px;
line-height: 1.5;
color: var(--mut);
}
.cpl-16__item>span {
margin-left: auto;
flex: none;
font-size: 10.5px;
font-weight: 700;
letter-spacing: .05em;
padding: 4px 9px;
border-radius: 99px;
border: 1px solid var(--line);
color: var(--mut);
}
@container cpl16 (width < 620px) {
.cpl-16__shell {
grid-template-columns: 1fr;
grid-template-rows: var(--headerH) auto 1fr;
grid-template-areas: "header" "side" "main";
}
.cpl-16__side {
height: auto;
flex-direction: row;
align-items: center;
padding: 10px 16px;
border-right: none;
border-bottom: 1px solid var(--line);
background: color-mix(in oklch,var(--bg) 82%,transparent);
backdrop-filter: blur(10px);
z-index: 2;
}
.cpl-16__side nav {
display: flex;
gap: 4px;
overflow-x: auto;
}
.cpl-16__side nav a {
white-space: nowrap;
border-left: none;
}
.cpl-16__count {
display: none;
}
.cpl-16__item>span {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.cpl-16 * {
transition-duration: .01ms !important;
}
}.cpl-16,
.cpl-16 *,
.cpl-16 *::before,
.cpl-16 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cpl-16 {
--headerH: 58px;
--bg: oklch(0.975 0.005 100);
--ink: oklch(0.24 0.02 280);
--mut: oklch(0.54 0.015 280);
--line: oklch(0.89 0.008 280);
--acc: oklch(0.6 0.17 300);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
}
.cpl-16__stage {
width: 100%;
container: cpl16/inline-size;
height: 100vh;
height: 100svh;
overflow-y: auto;
background: var(--bg);
}
.cpl-16__shell {
display: grid;
min-height: 100%;
grid-template-columns: 200px 1fr;
grid-template-rows: var(--headerH) 1fr;
grid-template-areas: "header header" "side main";
}
.cpl-16__header {
grid-area: header;
position: sticky;
top: 0;
z-index: 3;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 22px;
background: color-mix(in oklch,var(--bg) 82%,transparent);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.cpl-16__brand {
display: flex;
align-items: center;
gap: 9px;
font-size: 14px;
font-weight: 800;
letter-spacing: -.01em;
}
.cpl-16__brand i {
width: 11px;
height: 11px;
border-radius: 50%;
background: conic-gradient(from 120deg,var(--acc),oklch(0.75 0.12 200),var(--acc));
}
.cpl-16__hire {
display: grid;
place-items: center;
min-height: 36px;
padding: 0 15px;
border-radius: 99px;
font-size: 12.5px;
font-weight: 700;
text-decoration: none;
color: oklch(0.99 0 0);
background: var(--ink);
transition: background .2s,translate .2s;
}
.cpl-16__hire:hover,
.cpl-16__hire:focus-visible {
background: var(--acc);
translate: 0 -1px;
}
.cpl-16__hire:focus-visible {
outline: 2px solid var(--acc);
outline-offset: 3px;
}
.cpl-16__side {
grid-area: side;
position: sticky;
top: var(--headerH);
align-self: start;
height: calc(100vh - var(--headerH));
height: calc(100svh - var(--headerH));
display: flex;
flex-direction: column;
gap: 14px;
padding: 22px 18px;
border-right: 1px solid var(--line);
}
.cpl-16__side nav {
display: grid;
gap: 2px;
}
.cpl-16__side nav a {
display: flex;
align-items: center;
min-height: 38px;
padding: 0 12px;
border-radius: 9px;
font-size: 13.5px;
font-weight: 600;
text-decoration: none;
color: var(--mut);
border-left: 2px solid transparent;
transition: color .2s,background .2s,border-color .2s;
}
.cpl-16__side nav a:hover,
.cpl-16__side nav a:focus-visible {
color: var(--ink);
background: oklch(0.93 0.01 100);
}
.cpl-16__side nav a[aria-current] {
color: var(--ink);
border-left-color: var(--acc);
background: oklch(0.94 0.015 300);
}
.cpl-16__side nav a:focus-visible {
outline: 2px solid var(--acc);
outline-offset: -2px;
}
.cpl-16__count {
margin-top: auto;
font-size: 11.5px;
color: var(--mut);
}
.cpl-16__main {
grid-area: main;
padding: 22px 24px 34px;
display: grid;
gap: 12px;
align-content: start;
}
.cpl-16__main h3 {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--mut);
margin-top: 14px;
scroll-margin-top: calc(var(--headerH) + 12px);
}
.cpl-16__main h3:first-child {
margin-top: 0;
}
.cpl-16__item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px;
margin: 0 -12px;
border-radius: 14px;
text-decoration: none;
color: inherit;
transition: background .2s;
}
.cpl-16__item:hover,
.cpl-16__item:focus-visible {
background: oklch(0.94 0.008 100);
}
.cpl-16__item:focus-visible {
outline: 2px solid var(--acc);
outline-offset: -2px;
}
.cpl-16__item figure {
flex: none;
width: 92px;
aspect-ratio: 4/3;
border-radius: 10px;
background-image: var(--img,none),linear-gradient(140deg,var(--a),var(--b));
background-size: cover;
background-position: center;
}
.cpl-16__item h4 {
font-size: 14.5px;
letter-spacing: -.01em;
}
.cpl-16__item p {
margin-top: 3px;
font-size: 12.5px;
line-height: 1.5;
color: var(--mut);
}
.cpl-16__item>span {
margin-left: auto;
flex: none;
font-size: 10.5px;
font-weight: 700;
letter-spacing: .05em;
padding: 4px 9px;
border-radius: 99px;
border: 1px solid var(--line);
color: var(--mut);
}
@container cpl16 (width < 620px) {
.cpl-16__shell {
grid-template-columns: 1fr;
grid-template-rows: var(--headerH) auto 1fr;
grid-template-areas: "header" "side" "main";
}
.cpl-16__side {
height: auto;
flex-direction: row;
align-items: center;
padding: 10px 16px;
border-right: none;
border-bottom: 1px solid var(--line);
background: color-mix(in oklch,var(--bg) 82%,transparent);
backdrop-filter: blur(10px);
z-index: 2;
}
.cpl-16__side nav {
display: flex;
gap: 4px;
overflow-x: auto;
}
.cpl-16__side nav a {
white-space: nowrap;
border-left: none;
}
.cpl-16__count {
display: none;
}
.cpl-16__item>span {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.cpl-16 * {
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: Sticky Header Sidebar Portfolio Layout Template CSS
Source: https://codefronts.com/layouts/css-portfolio-layouts/sticky-header-sidebar-portfolio-layout-template-css/
The full app-shell template: a sticky header across the top, a sticky section rail down the left, and a long project list scrolling between them — the modern 'holy grail' with grid-template-areas naming the regions and two cooperating stickies doing what fixed positioning used to fake. The only trick worth memorizing is the offset handshake: the sidebar sticks at top: var(--headerH), so the two never fight.
## HTML
```html
<section class="cpl-16" aria-label="Sticky header and sidebar portfolio template demo">
<div class="cpl-16__stage">
<div class="cpl-16__shell">
<header class="cpl-16__header">
<span class="cpl-16__brand"><i aria-hidden="true"></i>Archive of Work</span>
<a class="cpl-16__hire" href="#hire">Hire me</a>
</header>
<aside class="cpl-16__side">
<nav aria-label="Sections">
<a href="#cpl16-2026" aria-current="true">2026</a>
<a href="#cpl16-2025">2025</a>
<a href="#cpl16-2024">2024</a>
<a href="#cpl16-experiments">Experiments</a>
</nav>
<p class="cpl-16__count">23 projects · 2019—2026</p>
</aside>
<main class="cpl-16__main">
<h3 id="cpl16-2026">2026</h3>
<a class="cpl-16__item" href="#halcyon" style="--img:url('https://images.unsplash.com/photo-1481627834876-b7833e8f5570?q=80&w=900&auto=format&fit=crop');--a:oklch(0.78 0.11 280);--b:oklch(0.5 0.15 305)"><figure></figure><div><h4>Halcyon Reader</h4><p>RSS client & brand — calm software, calmer landing page.</p></div><span>Web</span></a>
<a class="cpl-16__item" href="#matterhorn" style="--img:url('https://images.unsplash.com/photo-1551632811-561732d1e306?q=80&w=900&auto=format&fit=crop');--a:oklch(0.82 0.09 70);--b:oklch(0.55 0.14 45)"><figure></figure><div><h4>Matterhorn Gear Co.</h4><p>Storefront rebuild — 2.1s LCP to 0.8s on the same catalogue.</p></div><span>E-comm</span></a>
<h3 id="cpl16-2025">2025</h3>
<a class="cpl-16__item" href="#quill" style="--img:url('https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=900&auto=format&fit=crop');--a:oklch(0.8 0.1 160);--b:oklch(0.52 0.12 185)"><figure></figure><div><h4>Quill Grants</h4><p>Application portal for a state arts fund — WCAG 2.2 AA end to end.</p></div><span>Gov</span></a>
<a class="cpl-16__item" href="#bellweather" style="--img:url('https://images.unsplash.com/photo-1478737270239-2f02b77fc618?q=80&w=900&auto=format&fit=crop');--a:oklch(0.76 0.12 20);--b:oklch(0.5 0.16 350)"><figure></figure><div><h4>Bellweather Radio</h4><p>Live player and 40-year episode archive, searchable in 90ms.</p></div><span>Media</span></a>
<h3 id="cpl16-2024">2024</h3>
<a class="cpl-16__item" href="#cartog" style="--img:url('https://images.unsplash.com/photo-1524661135-423995f22d0b?q=80&w=900&auto=format&fit=crop');--a:oklch(0.84 0.07 230);--b:oklch(0.56 0.11 260)"><figure></figure><div><h4>Cartog</h4><p>Map-annotation tool for field researchers; offline-first.</p></div><span>Tool</span></a>
<h3 id="cpl16-experiments">Experiments</h3>
<a class="cpl-16__item" href="#typetheremin" style="--img:url('https://images.unsplash.com/photo-1511512578047-dfb367046420?q=80&w=900&auto=format&fit=crop');--a:oklch(0.7 0.13 320);--b:oklch(0.45 0.14 345)"><figure></figure><div><h4>Type Theremin</h4><p>Variable-font weight played by cursor position. Useless, beloved.</p></div><span>Play</span></a>
</main>
</div>
</div>
</section>
```
## CSS
```css
.cpl-16,
.cpl-16 *,
.cpl-16 *::before,
.cpl-16 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cpl-16 {
--headerH: 58px;
--bg: oklch(0.975 0.005 100);
--ink: oklch(0.24 0.02 280);
--mut: oklch(0.54 0.015 280);
--line: oklch(0.89 0.008 280);
--acc: oklch(0.6 0.17 300);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
}
.cpl-16__stage {
width: 100%;
container: cpl16/inline-size;
height: 100vh;
height: 100svh;
overflow-y: auto;
background: var(--bg);
}
.cpl-16__shell {
display: grid;
min-height: 100%;
grid-template-columns: 200px 1fr;
grid-template-rows: var(--headerH) 1fr;
grid-template-areas: "header header" "side main";
}
.cpl-16__header {
grid-area: header;
position: sticky;
top: 0;
z-index: 3;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 22px;
background: color-mix(in oklch,var(--bg) 82%,transparent);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.cpl-16__brand {
display: flex;
align-items: center;
gap: 9px;
font-size: 14px;
font-weight: 800;
letter-spacing: -.01em;
}
.cpl-16__brand i {
width: 11px;
height: 11px;
border-radius: 50%;
background: conic-gradient(from 120deg,var(--acc),oklch(0.75 0.12 200),var(--acc));
}
.cpl-16__hire {
display: grid;
place-items: center;
min-height: 36px;
padding: 0 15px;
border-radius: 99px;
font-size: 12.5px;
font-weight: 700;
text-decoration: none;
color: oklch(0.99 0 0);
background: var(--ink);
transition: background .2s,translate .2s;
}
.cpl-16__hire:hover,
.cpl-16__hire:focus-visible {
background: var(--acc);
translate: 0 -1px;
}
.cpl-16__hire:focus-visible {
outline: 2px solid var(--acc);
outline-offset: 3px;
}
.cpl-16__side {
grid-area: side;
position: sticky;
top: var(--headerH);
align-self: start;
height: calc(100vh - var(--headerH));
height: calc(100svh - var(--headerH));
display: flex;
flex-direction: column;
gap: 14px;
padding: 22px 18px;
border-right: 1px solid var(--line);
}
.cpl-16__side nav {
display: grid;
gap: 2px;
}
.cpl-16__side nav a {
display: flex;
align-items: center;
min-height: 38px;
padding: 0 12px;
border-radius: 9px;
font-size: 13.5px;
font-weight: 600;
text-decoration: none;
color: var(--mut);
border-left: 2px solid transparent;
transition: color .2s,background .2s,border-color .2s;
}
.cpl-16__side nav a:hover,
.cpl-16__side nav a:focus-visible {
color: var(--ink);
background: oklch(0.93 0.01 100);
}
.cpl-16__side nav a[aria-current] {
color: var(--ink);
border-left-color: var(--acc);
background: oklch(0.94 0.015 300);
}
.cpl-16__side nav a:focus-visible {
outline: 2px solid var(--acc);
outline-offset: -2px;
}
.cpl-16__count {
margin-top: auto;
font-size: 11.5px;
color: var(--mut);
}
.cpl-16__main {
grid-area: main;
padding: 22px 24px 34px;
display: grid;
gap: 12px;
align-content: start;
}
.cpl-16__main h3 {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--mut);
margin-top: 14px;
scroll-margin-top: calc(var(--headerH) + 12px);
}
.cpl-16__main h3:first-child {
margin-top: 0;
}
.cpl-16__item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px;
margin: 0 -12px;
border-radius: 14px;
text-decoration: none;
color: inherit;
transition: background .2s;
}
.cpl-16__item:hover,
.cpl-16__item:focus-visible {
background: oklch(0.94 0.008 100);
}
.cpl-16__item:focus-visible {
outline: 2px solid var(--acc);
outline-offset: -2px;
}
.cpl-16__item figure {
flex: none;
width: 92px;
aspect-ratio: 4/3;
border-radius: 10px;
background-image: var(--img,none),linear-gradient(140deg,var(--a),var(--b));
background-size: cover;
background-position: center;
}
.cpl-16__item h4 {
font-size: 14.5px;
letter-spacing: -.01em;
}
.cpl-16__item p {
margin-top: 3px;
font-size: 12.5px;
line-height: 1.5;
color: var(--mut);
}
.cpl-16__item>span {
margin-left: auto;
flex: none;
font-size: 10.5px;
font-weight: 700;
letter-spacing: .05em;
padding: 4px 9px;
border-radius: 99px;
border: 1px solid var(--line);
color: var(--mut);
}
@container cpl16 (width < 620px) {
.cpl-16__shell {
grid-template-columns: 1fr;
grid-template-rows: var(--headerH) auto 1fr;
grid-template-areas: "header" "side" "main";
}
.cpl-16__side {
height: auto;
flex-direction: row;
align-items: center;
padding: 10px 16px;
border-right: none;
border-bottom: 1px solid var(--line);
background: color-mix(in oklch,var(--bg) 82%,transparent);
backdrop-filter: blur(10px);
z-index: 2;
}
.cpl-16__side nav {
display: flex;
gap: 4px;
overflow-x: auto;
}
.cpl-16__side nav a {
white-space: nowrap;
border-left: none;
}
.cpl-16__count {
display: none;
}
.cpl-16__item>span {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.cpl-16 * {
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: Sticky Header Sidebar Portfolio Layout Template CSS
Source: https://codefronts.com/layouts/css-portfolio-layouts/sticky-header-sidebar-portfolio-layout-template-css/
The full app-shell template: a sticky header across the top, a sticky section rail down the left, and a long project list scrolling between them — the modern 'holy grail' with grid-template-areas naming the regions and two cooperating stickies doing what fixed positioning used to fake. The only trick worth memorizing is the offset handshake: the sidebar sticks at top: var(--headerH), so the two never fight.
## HTML
```html
<section class="cpl-16" aria-label="Sticky header and sidebar portfolio template demo">
<div class="cpl-16__stage">
<div class="cpl-16__shell">
<header class="cpl-16__header">
<span class="cpl-16__brand"><i aria-hidden="true"></i>Archive of Work</span>
<a class="cpl-16__hire" href="#hire">Hire me</a>
</header>
<aside class="cpl-16__side">
<nav aria-label="Sections">
<a href="#cpl16-2026" aria-current="true">2026</a>
<a href="#cpl16-2025">2025</a>
<a href="#cpl16-2024">2024</a>
<a href="#cpl16-experiments">Experiments</a>
</nav>
<p class="cpl-16__count">23 projects · 2019—2026</p>
</aside>
<main class="cpl-16__main">
<h3 id="cpl16-2026">2026</h3>
<a class="cpl-16__item" href="#halcyon" style="--img:url('https://images.unsplash.com/photo-1481627834876-b7833e8f5570?q=80&w=900&auto=format&fit=crop');--a:oklch(0.78 0.11 280);--b:oklch(0.5 0.15 305)"><figure></figure><div><h4>Halcyon Reader</h4><p>RSS client & brand — calm software, calmer landing page.</p></div><span>Web</span></a>
<a class="cpl-16__item" href="#matterhorn" style="--img:url('https://images.unsplash.com/photo-1551632811-561732d1e306?q=80&w=900&auto=format&fit=crop');--a:oklch(0.82 0.09 70);--b:oklch(0.55 0.14 45)"><figure></figure><div><h4>Matterhorn Gear Co.</h4><p>Storefront rebuild — 2.1s LCP to 0.8s on the same catalogue.</p></div><span>E-comm</span></a>
<h3 id="cpl16-2025">2025</h3>
<a class="cpl-16__item" href="#quill" style="--img:url('https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=900&auto=format&fit=crop');--a:oklch(0.8 0.1 160);--b:oklch(0.52 0.12 185)"><figure></figure><div><h4>Quill Grants</h4><p>Application portal for a state arts fund — WCAG 2.2 AA end to end.</p></div><span>Gov</span></a>
<a class="cpl-16__item" href="#bellweather" style="--img:url('https://images.unsplash.com/photo-1478737270239-2f02b77fc618?q=80&w=900&auto=format&fit=crop');--a:oklch(0.76 0.12 20);--b:oklch(0.5 0.16 350)"><figure></figure><div><h4>Bellweather Radio</h4><p>Live player and 40-year episode archive, searchable in 90ms.</p></div><span>Media</span></a>
<h3 id="cpl16-2024">2024</h3>
<a class="cpl-16__item" href="#cartog" style="--img:url('https://images.unsplash.com/photo-1524661135-423995f22d0b?q=80&w=900&auto=format&fit=crop');--a:oklch(0.84 0.07 230);--b:oklch(0.56 0.11 260)"><figure></figure><div><h4>Cartog</h4><p>Map-annotation tool for field researchers; offline-first.</p></div><span>Tool</span></a>
<h3 id="cpl16-experiments">Experiments</h3>
<a class="cpl-16__item" href="#typetheremin" style="--img:url('https://images.unsplash.com/photo-1511512578047-dfb367046420?q=80&w=900&auto=format&fit=crop');--a:oklch(0.7 0.13 320);--b:oklch(0.45 0.14 345)"><figure></figure><div><h4>Type Theremin</h4><p>Variable-font weight played by cursor position. Useless, beloved.</p></div><span>Play</span></a>
</main>
</div>
</div>
</section>
```
## CSS
```css
.cpl-16,
.cpl-16 *,
.cpl-16 *::before,
.cpl-16 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cpl-16 {
--headerH: 58px;
--bg: oklch(0.975 0.005 100);
--ink: oklch(0.24 0.02 280);
--mut: oklch(0.54 0.015 280);
--line: oklch(0.89 0.008 280);
--acc: oklch(0.6 0.17 300);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
}
.cpl-16__stage {
width: 100%;
container: cpl16/inline-size;
height: 100vh;
height: 100svh;
overflow-y: auto;
background: var(--bg);
}
.cpl-16__shell {
display: grid;
min-height: 100%;
grid-template-columns: 200px 1fr;
grid-template-rows: var(--headerH) 1fr;
grid-template-areas: "header header" "side main";
}
.cpl-16__header {
grid-area: header;
position: sticky;
top: 0;
z-index: 3;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 22px;
background: color-mix(in oklch,var(--bg) 82%,transparent);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.cpl-16__brand {
display: flex;
align-items: center;
gap: 9px;
font-size: 14px;
font-weight: 800;
letter-spacing: -.01em;
}
.cpl-16__brand i {
width: 11px;
height: 11px;
border-radius: 50%;
background: conic-gradient(from 120deg,var(--acc),oklch(0.75 0.12 200),var(--acc));
}
.cpl-16__hire {
display: grid;
place-items: center;
min-height: 36px;
padding: 0 15px;
border-radius: 99px;
font-size: 12.5px;
font-weight: 700;
text-decoration: none;
color: oklch(0.99 0 0);
background: var(--ink);
transition: background .2s,translate .2s;
}
.cpl-16__hire:hover,
.cpl-16__hire:focus-visible {
background: var(--acc);
translate: 0 -1px;
}
.cpl-16__hire:focus-visible {
outline: 2px solid var(--acc);
outline-offset: 3px;
}
.cpl-16__side {
grid-area: side;
position: sticky;
top: var(--headerH);
align-self: start;
height: calc(100vh - var(--headerH));
height: calc(100svh - var(--headerH));
display: flex;
flex-direction: column;
gap: 14px;
padding: 22px 18px;
border-right: 1px solid var(--line);
}
.cpl-16__side nav {
display: grid;
gap: 2px;
}
.cpl-16__side nav a {
display: flex;
align-items: center;
min-height: 38px;
padding: 0 12px;
border-radius: 9px;
font-size: 13.5px;
font-weight: 600;
text-decoration: none;
color: var(--mut);
border-left: 2px solid transparent;
transition: color .2s,background .2s,border-color .2s;
}
.cpl-16__side nav a:hover,
.cpl-16__side nav a:focus-visible {
color: var(--ink);
background: oklch(0.93 0.01 100);
}
.cpl-16__side nav a[aria-current] {
color: var(--ink);
border-left-color: var(--acc);
background: oklch(0.94 0.015 300);
}
.cpl-16__side nav a:focus-visible {
outline: 2px solid var(--acc);
outline-offset: -2px;
}
.cpl-16__count {
margin-top: auto;
font-size: 11.5px;
color: var(--mut);
}
.cpl-16__main {
grid-area: main;
padding: 22px 24px 34px;
display: grid;
gap: 12px;
align-content: start;
}
.cpl-16__main h3 {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--mut);
margin-top: 14px;
scroll-margin-top: calc(var(--headerH) + 12px);
}
.cpl-16__main h3:first-child {
margin-top: 0;
}
.cpl-16__item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px;
margin: 0 -12px;
border-radius: 14px;
text-decoration: none;
color: inherit;
transition: background .2s;
}
.cpl-16__item:hover,
.cpl-16__item:focus-visible {
background: oklch(0.94 0.008 100);
}
.cpl-16__item:focus-visible {
outline: 2px solid var(--acc);
outline-offset: -2px;
}
.cpl-16__item figure {
flex: none;
width: 92px;
aspect-ratio: 4/3;
border-radius: 10px;
background-image: var(--img,none),linear-gradient(140deg,var(--a),var(--b));
background-size: cover;
background-position: center;
}
.cpl-16__item h4 {
font-size: 14.5px;
letter-spacing: -.01em;
}
.cpl-16__item p {
margin-top: 3px;
font-size: 12.5px;
line-height: 1.5;
color: var(--mut);
}
.cpl-16__item>span {
margin-left: auto;
flex: none;
font-size: 10.5px;
font-weight: 700;
letter-spacing: .05em;
padding: 4px 9px;
border-radius: 99px;
border: 1px solid var(--line);
color: var(--mut);
}
@container cpl16 (width < 620px) {
.cpl-16__shell {
grid-template-columns: 1fr;
grid-template-rows: var(--headerH) auto 1fr;
grid-template-areas: "header" "side" "main";
}
.cpl-16__side {
height: auto;
flex-direction: row;
align-items: center;
padding: 10px 16px;
border-right: none;
border-bottom: 1px solid var(--line);
background: color-mix(in oklch,var(--bg) 82%,transparent);
backdrop-filter: blur(10px);
z-index: 2;
}
.cpl-16__side nav {
display: flex;
gap: 4px;
overflow-x: auto;
}
.cpl-16__side nav a {
white-space: nowrap;
border-left: none;
}
.cpl-16__count {
display: none;
}
.cpl-16__item>span {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.cpl-16 * {
transition-duration: .01ms !important;
}
}
```How this works
The shell is drawn once, in ASCII, in the CSS — which is why template areas beat coordinates for layouts humans maintain:
.shell{ display:grid; min-height:100%;
grid-template-columns: 200px 1fr;
grid-template-rows: var(--headerH) 1fr;
grid-template-areas:
'header header'
'side main'; }
.header{ grid-area:header; position:sticky; top:0; z-index:2; }
.side { grid-area:side; position:sticky;
top: var(--headerH); /* THE handshake */
height: calc(100svh - var(--headerH));
align-self:start; }Both bars are sticky in the same scroller; the header claims top:0, and the sidebar's top equals the header's height so it pins exactly beneath it — one custom property keeps the pair honest forever. align-self:start keeps the sidebar from stretching (a stretched sticky can't stick, same physics as demo 04), and capping its height at viewport − header lets a long rail scroll internally instead of pushing the pin.
The header earns its z-index with a backdrop-filter blur so content sliding beneath it stays legible without a hard edge. The rail marks the active section with aria-current — the same attribute your scrollspy JS would target later, but as shipped the highlight works on click alone. At narrow container widths the shell re-areas itself: the rail becomes a horizontal chip row under the header (still sticky!), which is the honest mobile translation of a sidebar.
Make it yours
- The one property:
--headerHdrives the header row height, the sidebar offset and the mobile chip-row offset. Change it in exactly one place. - Rail width: the
200pxcolumn; the template-areas string makes a right-hand rail a two-word edit ('header header' / 'main side' + column swap). - Density: the project list is a plain grid of rows — drop in demo 04's thumb-row anatomy or demo 13's cards; the shell doesn't care what fills
main. - Scrollspy upgrade: add a 12-line IntersectionObserver that moves
aria-currentas sections pass under the header — the CSS highlight is already keyed to it.
Gotchas — read before shipping
- The sidebar MUST NOT sit inside an
overflow:hiddenwrapper (the classic 'why won't it stick') and MUST havealign-self:startor a height — stretched children can't stick. - Sticky elements need explicit
z-indexdiscipline: header above sidebar above content; without it, focus rings and dropdowns shear through the header. - Two nested scrollbars (rail + page) confuse users — cap the rail's height so IT scrolls only when genuinely taller than the viewport, which a 6-item nav never is.
- On phones, don't shrink the sidebar — re-area it. A 200px rail at 360px viewport leaves a 160px content column; the chip-row pattern is the correct fold.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 113+ | 111+ |
Template areas + sticky are universal (2017/2019); container queries, svh and oklch set the stated floor, each with a trivial fallback.