01 / 20
All 20 in 20 CSS Sidebar Navigation
- 01Accessible Sidebar Navigation ARIA Reference
- 02Sidebar Focus Trap and Restore on Close
- 03Inert Background Content Behind a Drawer
- 04Sidebar Navigation Is Not a Tab List
- 05Announcing Collapsed and Expanded Sidebar State
- 06Roving Tabindex for Sidebar Keyboard Navigation
- 07Persisting Sidebar Collapse State Across Reloads
- 08Sidebar Scroll Containment with Overscroll Behavior
- 09Nested Submenu Keyboard and Screen Reader Behavior
- 10Resizable Sidebar with a Keyboard Accessible Handle
- 11Collapsible Sidebar with Checkbox State
- 12Responsive Sidebar Breakpoint Behavior
- 13Icon Rail Sidebar with Accessible Labels
- 14Dashboard Sidebar with Current Page State
- 15Multi Level Sidebar Menu Disclosure Pattern
- 16Accordion Sidebar with Details and Summary
- 17Sticky Full Height Sidebar Layout Mechanics
- 18Floating Sidebar Stacking and Overlay Order
- 19Off Canvas Sidebar Drawer with Focus Return
- 20Sidebar Footer Pinning with Scrollable Nav
Accessible Sidebar Navigation ARIA Reference
The reference build the other nineteen demos borrow from. A <nav> with an accessible name, a real <ul> of links, aria-current="page" on the active item, and a disclosure control that carries its expanded state. The two mistakes seen most in review are the ones this markup is built to avoid: aria-selected borrowed from tabs, and aria-expanded parked on the sidebar rather than the control.
Mosaic by Cruip: a complete Tailwind admin dashboard template in HTML, React, Next.js and Vue · $69
Cruip · Mosaic by Cruip · Affiliate link
The code
<section class="sdb-01" aria-label="Basalt developer console">
<div class="sdb-01__theme">
<input class="sdb-01__theme-input" type="checkbox" id="sdb-01-theme">
<label class="sdb-01__theme-label" for="sdb-01-theme">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="sdb-01__sr">Toggle dark theme</span>
</label>
</div>
<div class="sdb-01__app">
<nav class="sdb-01__nav" aria-label="Basalt documentation">
<p class="sdb-01__brand">Basalt</p>
<ul class="sdb-01__list">
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h16M4 18h10" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
Overview</a></li>
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main" aria-current="page">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 19V9l7-5 7 5v10" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
Deployments</a></li>
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="7.5" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M12 8v8M8 12h8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
API keys <span class="sdb-01__count">4</span></a></li>
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 17V7l8-3 8 3v10l-8 3Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>
Environments</a></li>
</ul>
<details class="sdb-01__group" open>
<summary class="sdb-01__summary" aria-controls="sdb-01-sublist">
<svg class="sdb-01__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
Team settings
</summary>
<ul class="sdb-01__sublist" id="sdb-01-sublist">
<li class="sdb-01__li"><a class="sdb-01__link sdb-01__link--sub" href="#sdb-01-main">Members</a></li>
<li class="sdb-01__li"><a class="sdb-01__link sdb-01__link--sub" href="#sdb-01-main">Audit log</a></li>
<li class="sdb-01__li"><a class="sdb-01__link sdb-01__link--sub" href="#sdb-01-main">Billing</a></li>
</ul>
</details>
</nav>
<main class="sdb-01__main" id="sdb-01-main">
<header class="sdb-01__bar">
<p class="sdb-01__crumb">northwind-api / production</p>
<h2 class="sdb-01__title">Deployments</h2>
</header>
<div class="sdb-01__panel">
<table class="sdb-01__table">
<thead><tr><th scope="col">Build</th><th scope="col">Commit</th><th scope="col">Duration</th><th scope="col">State</th></tr></thead>
<tbody>
<tr><td>#2481</td><td>a17f0c9</td><td>1m 42s</td><td><span class="sdb-01__pill">Live</span></td></tr>
<tr><td>#2480</td><td>7b3d114</td><td>1m 39s</td><td>Rolled back</td></tr>
<tr><td>#2479</td><td>0c92ae4</td><td>2m 04s</td><td>Superseded</td></tr>
<tr><td>#2478</td><td>d4418f2</td><td>1m 51s</td><td>Superseded</td></tr>
</tbody>
</table>
</div>
</main>
</div>
</section><section class="sdb-01" aria-label="Basalt developer console">
<div class="sdb-01__theme">
<input class="sdb-01__theme-input" type="checkbox" id="sdb-01-theme">
<label class="sdb-01__theme-label" for="sdb-01-theme">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="sdb-01__sr">Toggle dark theme</span>
</label>
</div>
<div class="sdb-01__app">
<nav class="sdb-01__nav" aria-label="Basalt documentation">
<p class="sdb-01__brand">Basalt</p>
<ul class="sdb-01__list">
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h16M4 18h10" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
Overview</a></li>
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main" aria-current="page">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 19V9l7-5 7 5v10" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
Deployments</a></li>
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="7.5" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M12 8v8M8 12h8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
API keys <span class="sdb-01__count">4</span></a></li>
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 17V7l8-3 8 3v10l-8 3Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>
Environments</a></li>
</ul>
<details class="sdb-01__group" open>
<summary class="sdb-01__summary" aria-controls="sdb-01-sublist">
<svg class="sdb-01__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
Team settings
</summary>
<ul class="sdb-01__sublist" id="sdb-01-sublist">
<li class="sdb-01__li"><a class="sdb-01__link sdb-01__link--sub" href="#sdb-01-main">Members</a></li>
<li class="sdb-01__li"><a class="sdb-01__link sdb-01__link--sub" href="#sdb-01-main">Audit log</a></li>
<li class="sdb-01__li"><a class="sdb-01__link sdb-01__link--sub" href="#sdb-01-main">Billing</a></li>
</ul>
</details>
</nav>
<main class="sdb-01__main" id="sdb-01-main">
<header class="sdb-01__bar">
<p class="sdb-01__crumb">northwind-api / production</p>
<h2 class="sdb-01__title">Deployments</h2>
</header>
<div class="sdb-01__panel">
<table class="sdb-01__table">
<thead><tr><th scope="col">Build</th><th scope="col">Commit</th><th scope="col">Duration</th><th scope="col">State</th></tr></thead>
<tbody>
<tr><td>#2481</td><td>a17f0c9</td><td>1m 42s</td><td><span class="sdb-01__pill">Live</span></td></tr>
<tr><td>#2480</td><td>7b3d114</td><td>1m 39s</td><td>Rolled back</td></tr>
<tr><td>#2479</td><td>0c92ae4</td><td>2m 04s</td><td>Superseded</td></tr>
<tr><td>#2478</td><td>d4418f2</td><td>1m 51s</td><td>Superseded</td></tr>
</tbody>
</table>
</div>
</main>
</div>
</section>.sdb-01 {
--sdb-01-bg: #f3f5f8;
--sdb-01-surf: #ffffff;
--sdb-01-surf2: #e8ecf1;
--sdb-01-line: #d2d8e0;
--sdb-01-ink: #101419;
--sdb-01-mut: #5a6472;
--sdb-01-acc: #1b56d6;
--sdb-01-acc: oklch(48% 0.19 264);
--sdb-01-acc-soft: #e3eafd;
--sdb-01-acc-soft: oklch(94% 0.03 264);
--sdb-01-font-display: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
width: 100%;
min-height: 100vh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--sdb-01-bg);
color: var(--sdb-01-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.sdb-01 *,
.sdb-01 *::before,
.sdb-01 *::after {
box-sizing: border-box;
}
.sdb-01:has(.sdb-01__theme-input:checked) {
--sdb-01-bg: #0c1015;
--sdb-01-surf: #141a21;
--sdb-01-surf2: #1b222c;
--sdb-01-line: #29323d;
--sdb-01-ink: #eef1f5;
--sdb-01-mut: #97a2b2;
--sdb-01-acc: #8fa8ff;
--sdb-01-acc: oklch(78% 0.13 267);
--sdb-01-acc-soft: #1a2340;
--sdb-01-acc-soft: oklch(30% 0.07 267);
}
@media (prefers-color-scheme: dark) {
.sdb-01 {
--sdb-01-bg: #0c1015;
--sdb-01-surf: #141a21;
--sdb-01-surf2: #1b222c;
--sdb-01-line: #29323d;
--sdb-01-ink: #eef1f5;
--sdb-01-mut: #97a2b2;
--sdb-01-acc: #8fa8ff;
--sdb-01-acc: oklch(78% 0.13 267);
--sdb-01-acc-soft: #1a2340;
--sdb-01-acc-soft: oklch(30% 0.07 267);
}
.sdb-01:has(.sdb-01__theme-input:checked) {
--sdb-01-bg: #f3f5f8;
--sdb-01-surf: #ffffff;
--sdb-01-surf2: #e8ecf1;
--sdb-01-line: #d2d8e0;
--sdb-01-ink: #101419;
--sdb-01-mut: #5a6472;
--sdb-01-acc: #1b56d6;
--sdb-01-acc: oklch(48% 0.19 264);
--sdb-01-acc-soft: #e3eafd;
--sdb-01-acc-soft: oklch(94% 0.03 264);
}
}
@media (prefers-color-scheme: light) {
.sdb-01:has(.sdb-01__theme-input:checked) {
--sdb-01-bg: #0c1015;
--sdb-01-surf: #141a21;
--sdb-01-surf2: #1b222c;
--sdb-01-line: #29323d;
--sdb-01-ink: #eef1f5;
--sdb-01-mut: #97a2b2;
--sdb-01-acc: #8fa8ff;
--sdb-01-acc: oklch(78% 0.13 267);
--sdb-01-acc-soft: #1a2340;
--sdb-01-acc-soft: oklch(30% 0.07 267);
}
}
.sdb-01__theme {
position: absolute;
inset-block-start: 0.85rem;
inset-inline-end: 0.85rem;
z-index: 60;
}
.sdb-01__theme-input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.sdb-01__theme-label {
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 1px solid var(--sdb-01-line);
background: var(--sdb-01-surf);
color: var(--sdb-01-ink);
cursor: pointer;
}
.sdb-01__theme-label svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-01__theme-input:focus-visible + .sdb-01__theme-label {
outline: 2px solid var(--sdb-01-acc);
outline-offset: 2px;
}
.sdb-01__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-01__app {
display: grid;
grid-template-columns: minmax(0, 1fr);
min-block-size: 100vh;
}
@media (min-width: 48rem) {
.sdb-01__app {
grid-template-columns: 16.25rem minmax(0, 1fr);
}
}
.sdb-01__nav {
min-inline-size: 0;
padding: 1rem 0.75rem;
background: var(--sdb-01-surf);
border-block-end: 1px solid var(--sdb-01-line);
}
@media (min-width: 48rem) {
.sdb-01__nav {
border-block-end: 0;
border-inline-end: 1px solid var(--sdb-01-line);
}
}
.sdb-01__brand {
margin: 0 0 1rem;
padding-inline: 0.7rem;
font-family: var(--sdb-01-font-display);
font-size: 0.82rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--sdb-01-mut);
}
.sdb-01__list,
.sdb-01__sublist {
list-style: none;
margin: 0;
padding: 0;
}
.sdb-01__li {
min-inline-size: 0;
}
.sdb-01__link {
position: relative;
display: flex;
align-items: center;
gap: 0.65rem;
min-block-size: 2.75rem;
padding: 0.5rem 0.7rem;
color: var(--sdb-01-ink);
text-decoration: none;
font-size: 0.92rem;
border-inline-start: 2px solid transparent;
min-inline-size: 0;
}
.sdb-01__link svg {
inline-size: 1.1rem;
block-size: 1.1rem;
flex: none;
color: var(--sdb-01-mut);
}
.sdb-01__link:hover {
background: var(--sdb-01-surf2);
}
.sdb-01__link:focus-visible {
outline: 2px solid var(--sdb-01-acc);
outline-offset: -2px;
}
.sdb-01__link[aria-current="page"] {
background: var(--sdb-01-acc-soft);
border-inline-start-color: var(--sdb-01-acc);
color: var(--sdb-01-acc);
font-weight: 600;
}
.sdb-01__link[aria-current="page"] svg {
color: var(--sdb-01-acc);
}
.sdb-01__link--sub {
padding-inline-start: 2.4rem;
font-size: 0.88rem;
color: var(--sdb-01-mut);
}
.sdb-01__count {
margin-inline-start: auto;
font-family: var(--sdb-01-font-display);
font-size: 0.72rem;
padding: 0.1rem 0.4rem;
background: var(--sdb-01-surf2);
color: var(--sdb-01-mut);
}
.sdb-01__group {
margin-block-start: 0.6rem;
border-block-start: 1px solid var(--sdb-01-line);
}
.sdb-01__summary {
position: relative;
display: flex;
align-items: center;
gap: 0.65rem;
min-block-size: 2.75rem;
padding: 0.5rem 0.7rem;
cursor: pointer;
font-size: 0.92rem;
font-weight: 600;
list-style: none;
}
.sdb-01__summary::-webkit-details-marker {
display: none;
}
.sdb-01__summary:focus-visible {
outline: 2px solid var(--sdb-01-acc);
outline-offset: -2px;
}
.sdb-01__chev {
inline-size: 1rem;
block-size: 1rem;
flex: none;
color: var(--sdb-01-mut);
transition: transform 160ms ease;
}
.sdb-01__group[open] .sdb-01__chev {
transform: rotate(90deg);
}
.sdb-01__main {
min-inline-size: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
align-content: start;
}
.sdb-01__bar {
padding: 1.5rem 1.25rem 1.1rem;
border-block-end: 1px solid var(--sdb-01-line);
}
@media (min-width: 48rem) {
.sdb-01__bar {
padding: 2rem 2.25rem 1.4rem;
}
}
.sdb-01__crumb {
margin: 0 0 0.35rem;
font-family: var(--sdb-01-font-display);
font-size: 0.74rem;
letter-spacing: 0.06em;
color: var(--sdb-01-mut);
}
.sdb-01__title {
margin: 0;
font-size: clamp(1.35rem, 3.4vw, 1.9rem);
line-height: 1.15;
letter-spacing: -0.02em;
}
.sdb-01__panel {
padding: 1.25rem;
overflow-x: auto;
}
@media (min-width: 48rem) {
.sdb-01__panel {
padding: 1.75rem 2.25rem 2.5rem;
}
}
.sdb-01__table {
inline-size: 100%;
border-collapse: collapse;
font-size: 0.88rem;
background: var(--sdb-01-surf);
border: 1px solid var(--sdb-01-line);
}
.sdb-01__table th,
.sdb-01__table td {
padding: 0.7rem 0.9rem;
text-align: start;
border-block-end: 1px solid var(--sdb-01-line);
min-inline-size: 0;
}
.sdb-01__table th {
font-family: var(--sdb-01-font-display);
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--sdb-01-mut);
font-weight: 600;
}
.sdb-01__table td {
font-family: var(--sdb-01-font-display);
font-size: 0.82rem;
color: var(--sdb-01-mut);
}
.sdb-01__table tbody tr:last-child td {
border-block-end: 0;
}
.sdb-01__pill {
display: inline-block;
padding: 0.1rem 0.45rem;
background: var(--sdb-01-acc-soft);
color: var(--sdb-01-acc);
font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
.sdb-01__chev {
transition: none;
}
}.sdb-01 {
--sdb-01-bg: #f3f5f8;
--sdb-01-surf: #ffffff;
--sdb-01-surf2: #e8ecf1;
--sdb-01-line: #d2d8e0;
--sdb-01-ink: #101419;
--sdb-01-mut: #5a6472;
--sdb-01-acc: #1b56d6;
--sdb-01-acc: oklch(48% 0.19 264);
--sdb-01-acc-soft: #e3eafd;
--sdb-01-acc-soft: oklch(94% 0.03 264);
--sdb-01-font-display: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
width: 100%;
min-height: 100vh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--sdb-01-bg);
color: var(--sdb-01-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.sdb-01 *,
.sdb-01 *::before,
.sdb-01 *::after {
box-sizing: border-box;
}
.sdb-01:has(.sdb-01__theme-input:checked) {
--sdb-01-bg: #0c1015;
--sdb-01-surf: #141a21;
--sdb-01-surf2: #1b222c;
--sdb-01-line: #29323d;
--sdb-01-ink: #eef1f5;
--sdb-01-mut: #97a2b2;
--sdb-01-acc: #8fa8ff;
--sdb-01-acc: oklch(78% 0.13 267);
--sdb-01-acc-soft: #1a2340;
--sdb-01-acc-soft: oklch(30% 0.07 267);
}
@media (prefers-color-scheme: dark) {
.sdb-01 {
--sdb-01-bg: #0c1015;
--sdb-01-surf: #141a21;
--sdb-01-surf2: #1b222c;
--sdb-01-line: #29323d;
--sdb-01-ink: #eef1f5;
--sdb-01-mut: #97a2b2;
--sdb-01-acc: #8fa8ff;
--sdb-01-acc: oklch(78% 0.13 267);
--sdb-01-acc-soft: #1a2340;
--sdb-01-acc-soft: oklch(30% 0.07 267);
}
.sdb-01:has(.sdb-01__theme-input:checked) {
--sdb-01-bg: #f3f5f8;
--sdb-01-surf: #ffffff;
--sdb-01-surf2: #e8ecf1;
--sdb-01-line: #d2d8e0;
--sdb-01-ink: #101419;
--sdb-01-mut: #5a6472;
--sdb-01-acc: #1b56d6;
--sdb-01-acc: oklch(48% 0.19 264);
--sdb-01-acc-soft: #e3eafd;
--sdb-01-acc-soft: oklch(94% 0.03 264);
}
}
@media (prefers-color-scheme: light) {
.sdb-01:has(.sdb-01__theme-input:checked) {
--sdb-01-bg: #0c1015;
--sdb-01-surf: #141a21;
--sdb-01-surf2: #1b222c;
--sdb-01-line: #29323d;
--sdb-01-ink: #eef1f5;
--sdb-01-mut: #97a2b2;
--sdb-01-acc: #8fa8ff;
--sdb-01-acc: oklch(78% 0.13 267);
--sdb-01-acc-soft: #1a2340;
--sdb-01-acc-soft: oklch(30% 0.07 267);
}
}
.sdb-01__theme {
position: absolute;
inset-block-start: 0.85rem;
inset-inline-end: 0.85rem;
z-index: 60;
}
.sdb-01__theme-input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.sdb-01__theme-label {
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 1px solid var(--sdb-01-line);
background: var(--sdb-01-surf);
color: var(--sdb-01-ink);
cursor: pointer;
}
.sdb-01__theme-label svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-01__theme-input:focus-visible + .sdb-01__theme-label {
outline: 2px solid var(--sdb-01-acc);
outline-offset: 2px;
}
.sdb-01__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-01__app {
display: grid;
grid-template-columns: minmax(0, 1fr);
min-block-size: 100vh;
}
@media (min-width: 48rem) {
.sdb-01__app {
grid-template-columns: 16.25rem minmax(0, 1fr);
}
}
.sdb-01__nav {
min-inline-size: 0;
padding: 1rem 0.75rem;
background: var(--sdb-01-surf);
border-block-end: 1px solid var(--sdb-01-line);
}
@media (min-width: 48rem) {
.sdb-01__nav {
border-block-end: 0;
border-inline-end: 1px solid var(--sdb-01-line);
}
}
.sdb-01__brand {
margin: 0 0 1rem;
padding-inline: 0.7rem;
font-family: var(--sdb-01-font-display);
font-size: 0.82rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--sdb-01-mut);
}
.sdb-01__list,
.sdb-01__sublist {
list-style: none;
margin: 0;
padding: 0;
}
.sdb-01__li {
min-inline-size: 0;
}
.sdb-01__link {
position: relative;
display: flex;
align-items: center;
gap: 0.65rem;
min-block-size: 2.75rem;
padding: 0.5rem 0.7rem;
color: var(--sdb-01-ink);
text-decoration: none;
font-size: 0.92rem;
border-inline-start: 2px solid transparent;
min-inline-size: 0;
}
.sdb-01__link svg {
inline-size: 1.1rem;
block-size: 1.1rem;
flex: none;
color: var(--sdb-01-mut);
}
.sdb-01__link:hover {
background: var(--sdb-01-surf2);
}
.sdb-01__link:focus-visible {
outline: 2px solid var(--sdb-01-acc);
outline-offset: -2px;
}
.sdb-01__link[aria-current="page"] {
background: var(--sdb-01-acc-soft);
border-inline-start-color: var(--sdb-01-acc);
color: var(--sdb-01-acc);
font-weight: 600;
}
.sdb-01__link[aria-current="page"] svg {
color: var(--sdb-01-acc);
}
.sdb-01__link--sub {
padding-inline-start: 2.4rem;
font-size: 0.88rem;
color: var(--sdb-01-mut);
}
.sdb-01__count {
margin-inline-start: auto;
font-family: var(--sdb-01-font-display);
font-size: 0.72rem;
padding: 0.1rem 0.4rem;
background: var(--sdb-01-surf2);
color: var(--sdb-01-mut);
}
.sdb-01__group {
margin-block-start: 0.6rem;
border-block-start: 1px solid var(--sdb-01-line);
}
.sdb-01__summary {
position: relative;
display: flex;
align-items: center;
gap: 0.65rem;
min-block-size: 2.75rem;
padding: 0.5rem 0.7rem;
cursor: pointer;
font-size: 0.92rem;
font-weight: 600;
list-style: none;
}
.sdb-01__summary::-webkit-details-marker {
display: none;
}
.sdb-01__summary:focus-visible {
outline: 2px solid var(--sdb-01-acc);
outline-offset: -2px;
}
.sdb-01__chev {
inline-size: 1rem;
block-size: 1rem;
flex: none;
color: var(--sdb-01-mut);
transition: transform 160ms ease;
}
.sdb-01__group[open] .sdb-01__chev {
transform: rotate(90deg);
}
.sdb-01__main {
min-inline-size: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
align-content: start;
}
.sdb-01__bar {
padding: 1.5rem 1.25rem 1.1rem;
border-block-end: 1px solid var(--sdb-01-line);
}
@media (min-width: 48rem) {
.sdb-01__bar {
padding: 2rem 2.25rem 1.4rem;
}
}
.sdb-01__crumb {
margin: 0 0 0.35rem;
font-family: var(--sdb-01-font-display);
font-size: 0.74rem;
letter-spacing: 0.06em;
color: var(--sdb-01-mut);
}
.sdb-01__title {
margin: 0;
font-size: clamp(1.35rem, 3.4vw, 1.9rem);
line-height: 1.15;
letter-spacing: -0.02em;
}
.sdb-01__panel {
padding: 1.25rem;
overflow-x: auto;
}
@media (min-width: 48rem) {
.sdb-01__panel {
padding: 1.75rem 2.25rem 2.5rem;
}
}
.sdb-01__table {
inline-size: 100%;
border-collapse: collapse;
font-size: 0.88rem;
background: var(--sdb-01-surf);
border: 1px solid var(--sdb-01-line);
}
.sdb-01__table th,
.sdb-01__table td {
padding: 0.7rem 0.9rem;
text-align: start;
border-block-end: 1px solid var(--sdb-01-line);
min-inline-size: 0;
}
.sdb-01__table th {
font-family: var(--sdb-01-font-display);
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--sdb-01-mut);
font-weight: 600;
}
.sdb-01__table td {
font-family: var(--sdb-01-font-display);
font-size: 0.82rem;
color: var(--sdb-01-mut);
}
.sdb-01__table tbody tr:last-child td {
border-block-end: 0;
}
.sdb-01__pill {
display: inline-block;
padding: 0.1rem 0.45rem;
background: var(--sdb-01-acc-soft);
color: var(--sdb-01-acc);
font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
.sdb-01__chev {
transition: none;
}
}Here's a working CSS Sidebar Navigation 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: Accessible Sidebar Navigation ARIA Reference
Source: https://codefronts.com/navigation/css-sidebar-navigation/accessible-sidebar-navigation-aria-reference/
The reference build the other nineteen demos borrow from. A <nav> with an accessible name, a real <ul> of links, aria-current="page" on the active item, and a disclosure control that carries its expanded state. The two mistakes seen most in review are the ones this markup is built to avoid: aria-selected borrowed from tabs, and aria-expanded parked on the sidebar rather than the control.
## HTML
```html
<section class="sdb-01" aria-label="Basalt developer console">
<div class="sdb-01__theme">
<input class="sdb-01__theme-input" type="checkbox" id="sdb-01-theme">
<label class="sdb-01__theme-label" for="sdb-01-theme">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="sdb-01__sr">Toggle dark theme</span>
</label>
</div>
<div class="sdb-01__app">
<nav class="sdb-01__nav" aria-label="Basalt documentation">
<p class="sdb-01__brand">Basalt</p>
<ul class="sdb-01__list">
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h16M4 18h10" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
Overview</a></li>
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main" aria-current="page">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 19V9l7-5 7 5v10" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
Deployments</a></li>
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="7.5" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M12 8v8M8 12h8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
API keys <span class="sdb-01__count">4</span></a></li>
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 17V7l8-3 8 3v10l-8 3Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>
Environments</a></li>
</ul>
<details class="sdb-01__group" open>
<summary class="sdb-01__summary" aria-controls="sdb-01-sublist">
<svg class="sdb-01__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
Team settings
</summary>
<ul class="sdb-01__sublist" id="sdb-01-sublist">
<li class="sdb-01__li"><a class="sdb-01__link sdb-01__link--sub" href="#sdb-01-main">Members</a></li>
<li class="sdb-01__li"><a class="sdb-01__link sdb-01__link--sub" href="#sdb-01-main">Audit log</a></li>
<li class="sdb-01__li"><a class="sdb-01__link sdb-01__link--sub" href="#sdb-01-main">Billing</a></li>
</ul>
</details>
</nav>
<main class="sdb-01__main" id="sdb-01-main">
<header class="sdb-01__bar">
<p class="sdb-01__crumb">northwind-api / production</p>
<h2 class="sdb-01__title">Deployments</h2>
</header>
<div class="sdb-01__panel">
<table class="sdb-01__table">
<thead><tr><th scope="col">Build</th><th scope="col">Commit</th><th scope="col">Duration</th><th scope="col">State</th></tr></thead>
<tbody>
<tr><td>#2481</td><td>a17f0c9</td><td>1m 42s</td><td><span class="sdb-01__pill">Live</span></td></tr>
<tr><td>#2480</td><td>7b3d114</td><td>1m 39s</td><td>Rolled back</td></tr>
<tr><td>#2479</td><td>0c92ae4</td><td>2m 04s</td><td>Superseded</td></tr>
<tr><td>#2478</td><td>d4418f2</td><td>1m 51s</td><td>Superseded</td></tr>
</tbody>
</table>
</div>
</main>
</div>
</section>
```
## CSS
```css
.sdb-01 {
--sdb-01-bg: #f3f5f8;
--sdb-01-surf: #ffffff;
--sdb-01-surf2: #e8ecf1;
--sdb-01-line: #d2d8e0;
--sdb-01-ink: #101419;
--sdb-01-mut: #5a6472;
--sdb-01-acc: #1b56d6;
--sdb-01-acc: oklch(48% 0.19 264);
--sdb-01-acc-soft: #e3eafd;
--sdb-01-acc-soft: oklch(94% 0.03 264);
--sdb-01-font-display: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
width: 100%;
min-height: 100vh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--sdb-01-bg);
color: var(--sdb-01-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.sdb-01 *,
.sdb-01 *::before,
.sdb-01 *::after {
box-sizing: border-box;
}
.sdb-01:has(.sdb-01__theme-input:checked) {
--sdb-01-bg: #0c1015;
--sdb-01-surf: #141a21;
--sdb-01-surf2: #1b222c;
--sdb-01-line: #29323d;
--sdb-01-ink: #eef1f5;
--sdb-01-mut: #97a2b2;
--sdb-01-acc: #8fa8ff;
--sdb-01-acc: oklch(78% 0.13 267);
--sdb-01-acc-soft: #1a2340;
--sdb-01-acc-soft: oklch(30% 0.07 267);
}
@media (prefers-color-scheme: dark) {
.sdb-01 {
--sdb-01-bg: #0c1015;
--sdb-01-surf: #141a21;
--sdb-01-surf2: #1b222c;
--sdb-01-line: #29323d;
--sdb-01-ink: #eef1f5;
--sdb-01-mut: #97a2b2;
--sdb-01-acc: #8fa8ff;
--sdb-01-acc: oklch(78% 0.13 267);
--sdb-01-acc-soft: #1a2340;
--sdb-01-acc-soft: oklch(30% 0.07 267);
}
.sdb-01:has(.sdb-01__theme-input:checked) {
--sdb-01-bg: #f3f5f8;
--sdb-01-surf: #ffffff;
--sdb-01-surf2: #e8ecf1;
--sdb-01-line: #d2d8e0;
--sdb-01-ink: #101419;
--sdb-01-mut: #5a6472;
--sdb-01-acc: #1b56d6;
--sdb-01-acc: oklch(48% 0.19 264);
--sdb-01-acc-soft: #e3eafd;
--sdb-01-acc-soft: oklch(94% 0.03 264);
}
}
@media (prefers-color-scheme: light) {
.sdb-01:has(.sdb-01__theme-input:checked) {
--sdb-01-bg: #0c1015;
--sdb-01-surf: #141a21;
--sdb-01-surf2: #1b222c;
--sdb-01-line: #29323d;
--sdb-01-ink: #eef1f5;
--sdb-01-mut: #97a2b2;
--sdb-01-acc: #8fa8ff;
--sdb-01-acc: oklch(78% 0.13 267);
--sdb-01-acc-soft: #1a2340;
--sdb-01-acc-soft: oklch(30% 0.07 267);
}
}
.sdb-01__theme {
position: absolute;
inset-block-start: 0.85rem;
inset-inline-end: 0.85rem;
z-index: 60;
}
.sdb-01__theme-input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.sdb-01__theme-label {
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 1px solid var(--sdb-01-line);
background: var(--sdb-01-surf);
color: var(--sdb-01-ink);
cursor: pointer;
}
.sdb-01__theme-label svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-01__theme-input:focus-visible + .sdb-01__theme-label {
outline: 2px solid var(--sdb-01-acc);
outline-offset: 2px;
}
.sdb-01__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-01__app {
display: grid;
grid-template-columns: minmax(0, 1fr);
min-block-size: 100vh;
}
@media (min-width: 48rem) {
.sdb-01__app {
grid-template-columns: 16.25rem minmax(0, 1fr);
}
}
.sdb-01__nav {
min-inline-size: 0;
padding: 1rem 0.75rem;
background: var(--sdb-01-surf);
border-block-end: 1px solid var(--sdb-01-line);
}
@media (min-width: 48rem) {
.sdb-01__nav {
border-block-end: 0;
border-inline-end: 1px solid var(--sdb-01-line);
}
}
.sdb-01__brand {
margin: 0 0 1rem;
padding-inline: 0.7rem;
font-family: var(--sdb-01-font-display);
font-size: 0.82rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--sdb-01-mut);
}
.sdb-01__list,
.sdb-01__sublist {
list-style: none;
margin: 0;
padding: 0;
}
.sdb-01__li {
min-inline-size: 0;
}
.sdb-01__link {
position: relative;
display: flex;
align-items: center;
gap: 0.65rem;
min-block-size: 2.75rem;
padding: 0.5rem 0.7rem;
color: var(--sdb-01-ink);
text-decoration: none;
font-size: 0.92rem;
border-inline-start: 2px solid transparent;
min-inline-size: 0;
}
.sdb-01__link svg {
inline-size: 1.1rem;
block-size: 1.1rem;
flex: none;
color: var(--sdb-01-mut);
}
.sdb-01__link:hover {
background: var(--sdb-01-surf2);
}
.sdb-01__link:focus-visible {
outline: 2px solid var(--sdb-01-acc);
outline-offset: -2px;
}
.sdb-01__link[aria-current="page"] {
background: var(--sdb-01-acc-soft);
border-inline-start-color: var(--sdb-01-acc);
color: var(--sdb-01-acc);
font-weight: 600;
}
.sdb-01__link[aria-current="page"] svg {
color: var(--sdb-01-acc);
}
.sdb-01__link--sub {
padding-inline-start: 2.4rem;
font-size: 0.88rem;
color: var(--sdb-01-mut);
}
.sdb-01__count {
margin-inline-start: auto;
font-family: var(--sdb-01-font-display);
font-size: 0.72rem;
padding: 0.1rem 0.4rem;
background: var(--sdb-01-surf2);
color: var(--sdb-01-mut);
}
.sdb-01__group {
margin-block-start: 0.6rem;
border-block-start: 1px solid var(--sdb-01-line);
}
.sdb-01__summary {
position: relative;
display: flex;
align-items: center;
gap: 0.65rem;
min-block-size: 2.75rem;
padding: 0.5rem 0.7rem;
cursor: pointer;
font-size: 0.92rem;
font-weight: 600;
list-style: none;
}
.sdb-01__summary::-webkit-details-marker {
display: none;
}
.sdb-01__summary:focus-visible {
outline: 2px solid var(--sdb-01-acc);
outline-offset: -2px;
}
.sdb-01__chev {
inline-size: 1rem;
block-size: 1rem;
flex: none;
color: var(--sdb-01-mut);
transition: transform 160ms ease;
}
.sdb-01__group[open] .sdb-01__chev {
transform: rotate(90deg);
}
.sdb-01__main {
min-inline-size: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
align-content: start;
}
.sdb-01__bar {
padding: 1.5rem 1.25rem 1.1rem;
border-block-end: 1px solid var(--sdb-01-line);
}
@media (min-width: 48rem) {
.sdb-01__bar {
padding: 2rem 2.25rem 1.4rem;
}
}
.sdb-01__crumb {
margin: 0 0 0.35rem;
font-family: var(--sdb-01-font-display);
font-size: 0.74rem;
letter-spacing: 0.06em;
color: var(--sdb-01-mut);
}
.sdb-01__title {
margin: 0;
font-size: clamp(1.35rem, 3.4vw, 1.9rem);
line-height: 1.15;
letter-spacing: -0.02em;
}
.sdb-01__panel {
padding: 1.25rem;
overflow-x: auto;
}
@media (min-width: 48rem) {
.sdb-01__panel {
padding: 1.75rem 2.25rem 2.5rem;
}
}
.sdb-01__table {
inline-size: 100%;
border-collapse: collapse;
font-size: 0.88rem;
background: var(--sdb-01-surf);
border: 1px solid var(--sdb-01-line);
}
.sdb-01__table th,
.sdb-01__table td {
padding: 0.7rem 0.9rem;
text-align: start;
border-block-end: 1px solid var(--sdb-01-line);
min-inline-size: 0;
}
.sdb-01__table th {
font-family: var(--sdb-01-font-display);
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--sdb-01-mut);
font-weight: 600;
}
.sdb-01__table td {
font-family: var(--sdb-01-font-display);
font-size: 0.82rem;
color: var(--sdb-01-mut);
}
.sdb-01__table tbody tr:last-child td {
border-block-end: 0;
}
.sdb-01__pill {
display: inline-block;
padding: 0.1rem 0.45rem;
background: var(--sdb-01-acc-soft);
color: var(--sdb-01-acc);
font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
.sdb-01__chev {
transition: none;
}
}
```Here's a working CSS Sidebar Navigation 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: Accessible Sidebar Navigation ARIA Reference
Source: https://codefronts.com/navigation/css-sidebar-navigation/accessible-sidebar-navigation-aria-reference/
The reference build the other nineteen demos borrow from. A <nav> with an accessible name, a real <ul> of links, aria-current="page" on the active item, and a disclosure control that carries its expanded state. The two mistakes seen most in review are the ones this markup is built to avoid: aria-selected borrowed from tabs, and aria-expanded parked on the sidebar rather than the control.
## HTML
```html
<section class="sdb-01" aria-label="Basalt developer console">
<div class="sdb-01__theme">
<input class="sdb-01__theme-input" type="checkbox" id="sdb-01-theme">
<label class="sdb-01__theme-label" for="sdb-01-theme">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="sdb-01__sr">Toggle dark theme</span>
</label>
</div>
<div class="sdb-01__app">
<nav class="sdb-01__nav" aria-label="Basalt documentation">
<p class="sdb-01__brand">Basalt</p>
<ul class="sdb-01__list">
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h16M4 18h10" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
Overview</a></li>
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main" aria-current="page">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 19V9l7-5 7 5v10" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
Deployments</a></li>
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="7.5" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M12 8v8M8 12h8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
API keys <span class="sdb-01__count">4</span></a></li>
<li class="sdb-01__li"><a class="sdb-01__link" href="#sdb-01-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 17V7l8-3 8 3v10l-8 3Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>
Environments</a></li>
</ul>
<details class="sdb-01__group" open>
<summary class="sdb-01__summary" aria-controls="sdb-01-sublist">
<svg class="sdb-01__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
Team settings
</summary>
<ul class="sdb-01__sublist" id="sdb-01-sublist">
<li class="sdb-01__li"><a class="sdb-01__link sdb-01__link--sub" href="#sdb-01-main">Members</a></li>
<li class="sdb-01__li"><a class="sdb-01__link sdb-01__link--sub" href="#sdb-01-main">Audit log</a></li>
<li class="sdb-01__li"><a class="sdb-01__link sdb-01__link--sub" href="#sdb-01-main">Billing</a></li>
</ul>
</details>
</nav>
<main class="sdb-01__main" id="sdb-01-main">
<header class="sdb-01__bar">
<p class="sdb-01__crumb">northwind-api / production</p>
<h2 class="sdb-01__title">Deployments</h2>
</header>
<div class="sdb-01__panel">
<table class="sdb-01__table">
<thead><tr><th scope="col">Build</th><th scope="col">Commit</th><th scope="col">Duration</th><th scope="col">State</th></tr></thead>
<tbody>
<tr><td>#2481</td><td>a17f0c9</td><td>1m 42s</td><td><span class="sdb-01__pill">Live</span></td></tr>
<tr><td>#2480</td><td>7b3d114</td><td>1m 39s</td><td>Rolled back</td></tr>
<tr><td>#2479</td><td>0c92ae4</td><td>2m 04s</td><td>Superseded</td></tr>
<tr><td>#2478</td><td>d4418f2</td><td>1m 51s</td><td>Superseded</td></tr>
</tbody>
</table>
</div>
</main>
</div>
</section>
```
## CSS
```css
.sdb-01 {
--sdb-01-bg: #f3f5f8;
--sdb-01-surf: #ffffff;
--sdb-01-surf2: #e8ecf1;
--sdb-01-line: #d2d8e0;
--sdb-01-ink: #101419;
--sdb-01-mut: #5a6472;
--sdb-01-acc: #1b56d6;
--sdb-01-acc: oklch(48% 0.19 264);
--sdb-01-acc-soft: #e3eafd;
--sdb-01-acc-soft: oklch(94% 0.03 264);
--sdb-01-font-display: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
width: 100%;
min-height: 100vh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--sdb-01-bg);
color: var(--sdb-01-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.sdb-01 *,
.sdb-01 *::before,
.sdb-01 *::after {
box-sizing: border-box;
}
.sdb-01:has(.sdb-01__theme-input:checked) {
--sdb-01-bg: #0c1015;
--sdb-01-surf: #141a21;
--sdb-01-surf2: #1b222c;
--sdb-01-line: #29323d;
--sdb-01-ink: #eef1f5;
--sdb-01-mut: #97a2b2;
--sdb-01-acc: #8fa8ff;
--sdb-01-acc: oklch(78% 0.13 267);
--sdb-01-acc-soft: #1a2340;
--sdb-01-acc-soft: oklch(30% 0.07 267);
}
@media (prefers-color-scheme: dark) {
.sdb-01 {
--sdb-01-bg: #0c1015;
--sdb-01-surf: #141a21;
--sdb-01-surf2: #1b222c;
--sdb-01-line: #29323d;
--sdb-01-ink: #eef1f5;
--sdb-01-mut: #97a2b2;
--sdb-01-acc: #8fa8ff;
--sdb-01-acc: oklch(78% 0.13 267);
--sdb-01-acc-soft: #1a2340;
--sdb-01-acc-soft: oklch(30% 0.07 267);
}
.sdb-01:has(.sdb-01__theme-input:checked) {
--sdb-01-bg: #f3f5f8;
--sdb-01-surf: #ffffff;
--sdb-01-surf2: #e8ecf1;
--sdb-01-line: #d2d8e0;
--sdb-01-ink: #101419;
--sdb-01-mut: #5a6472;
--sdb-01-acc: #1b56d6;
--sdb-01-acc: oklch(48% 0.19 264);
--sdb-01-acc-soft: #e3eafd;
--sdb-01-acc-soft: oklch(94% 0.03 264);
}
}
@media (prefers-color-scheme: light) {
.sdb-01:has(.sdb-01__theme-input:checked) {
--sdb-01-bg: #0c1015;
--sdb-01-surf: #141a21;
--sdb-01-surf2: #1b222c;
--sdb-01-line: #29323d;
--sdb-01-ink: #eef1f5;
--sdb-01-mut: #97a2b2;
--sdb-01-acc: #8fa8ff;
--sdb-01-acc: oklch(78% 0.13 267);
--sdb-01-acc-soft: #1a2340;
--sdb-01-acc-soft: oklch(30% 0.07 267);
}
}
.sdb-01__theme {
position: absolute;
inset-block-start: 0.85rem;
inset-inline-end: 0.85rem;
z-index: 60;
}
.sdb-01__theme-input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.sdb-01__theme-label {
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 1px solid var(--sdb-01-line);
background: var(--sdb-01-surf);
color: var(--sdb-01-ink);
cursor: pointer;
}
.sdb-01__theme-label svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-01__theme-input:focus-visible + .sdb-01__theme-label {
outline: 2px solid var(--sdb-01-acc);
outline-offset: 2px;
}
.sdb-01__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-01__app {
display: grid;
grid-template-columns: minmax(0, 1fr);
min-block-size: 100vh;
}
@media (min-width: 48rem) {
.sdb-01__app {
grid-template-columns: 16.25rem minmax(0, 1fr);
}
}
.sdb-01__nav {
min-inline-size: 0;
padding: 1rem 0.75rem;
background: var(--sdb-01-surf);
border-block-end: 1px solid var(--sdb-01-line);
}
@media (min-width: 48rem) {
.sdb-01__nav {
border-block-end: 0;
border-inline-end: 1px solid var(--sdb-01-line);
}
}
.sdb-01__brand {
margin: 0 0 1rem;
padding-inline: 0.7rem;
font-family: var(--sdb-01-font-display);
font-size: 0.82rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--sdb-01-mut);
}
.sdb-01__list,
.sdb-01__sublist {
list-style: none;
margin: 0;
padding: 0;
}
.sdb-01__li {
min-inline-size: 0;
}
.sdb-01__link {
position: relative;
display: flex;
align-items: center;
gap: 0.65rem;
min-block-size: 2.75rem;
padding: 0.5rem 0.7rem;
color: var(--sdb-01-ink);
text-decoration: none;
font-size: 0.92rem;
border-inline-start: 2px solid transparent;
min-inline-size: 0;
}
.sdb-01__link svg {
inline-size: 1.1rem;
block-size: 1.1rem;
flex: none;
color: var(--sdb-01-mut);
}
.sdb-01__link:hover {
background: var(--sdb-01-surf2);
}
.sdb-01__link:focus-visible {
outline: 2px solid var(--sdb-01-acc);
outline-offset: -2px;
}
.sdb-01__link[aria-current="page"] {
background: var(--sdb-01-acc-soft);
border-inline-start-color: var(--sdb-01-acc);
color: var(--sdb-01-acc);
font-weight: 600;
}
.sdb-01__link[aria-current="page"] svg {
color: var(--sdb-01-acc);
}
.sdb-01__link--sub {
padding-inline-start: 2.4rem;
font-size: 0.88rem;
color: var(--sdb-01-mut);
}
.sdb-01__count {
margin-inline-start: auto;
font-family: var(--sdb-01-font-display);
font-size: 0.72rem;
padding: 0.1rem 0.4rem;
background: var(--sdb-01-surf2);
color: var(--sdb-01-mut);
}
.sdb-01__group {
margin-block-start: 0.6rem;
border-block-start: 1px solid var(--sdb-01-line);
}
.sdb-01__summary {
position: relative;
display: flex;
align-items: center;
gap: 0.65rem;
min-block-size: 2.75rem;
padding: 0.5rem 0.7rem;
cursor: pointer;
font-size: 0.92rem;
font-weight: 600;
list-style: none;
}
.sdb-01__summary::-webkit-details-marker {
display: none;
}
.sdb-01__summary:focus-visible {
outline: 2px solid var(--sdb-01-acc);
outline-offset: -2px;
}
.sdb-01__chev {
inline-size: 1rem;
block-size: 1rem;
flex: none;
color: var(--sdb-01-mut);
transition: transform 160ms ease;
}
.sdb-01__group[open] .sdb-01__chev {
transform: rotate(90deg);
}
.sdb-01__main {
min-inline-size: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
align-content: start;
}
.sdb-01__bar {
padding: 1.5rem 1.25rem 1.1rem;
border-block-end: 1px solid var(--sdb-01-line);
}
@media (min-width: 48rem) {
.sdb-01__bar {
padding: 2rem 2.25rem 1.4rem;
}
}
.sdb-01__crumb {
margin: 0 0 0.35rem;
font-family: var(--sdb-01-font-display);
font-size: 0.74rem;
letter-spacing: 0.06em;
color: var(--sdb-01-mut);
}
.sdb-01__title {
margin: 0;
font-size: clamp(1.35rem, 3.4vw, 1.9rem);
line-height: 1.15;
letter-spacing: -0.02em;
}
.sdb-01__panel {
padding: 1.25rem;
overflow-x: auto;
}
@media (min-width: 48rem) {
.sdb-01__panel {
padding: 1.75rem 2.25rem 2.5rem;
}
}
.sdb-01__table {
inline-size: 100%;
border-collapse: collapse;
font-size: 0.88rem;
background: var(--sdb-01-surf);
border: 1px solid var(--sdb-01-line);
}
.sdb-01__table th,
.sdb-01__table td {
padding: 0.7rem 0.9rem;
text-align: start;
border-block-end: 1px solid var(--sdb-01-line);
min-inline-size: 0;
}
.sdb-01__table th {
font-family: var(--sdb-01-font-display);
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--sdb-01-mut);
font-weight: 600;
}
.sdb-01__table td {
font-family: var(--sdb-01-font-display);
font-size: 0.82rem;
color: var(--sdb-01-mut);
}
.sdb-01__table tbody tr:last-child td {
border-block-end: 0;
}
.sdb-01__pill {
display: inline-block;
padding: 0.1rem 0.45rem;
background: var(--sdb-01-acc-soft);
color: var(--sdb-01-acc);
font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
.sdb-01__chev {
transition: none;
}
}
```How this works
A sidebar is a list of links inside a named landmark, and almost everything else follows from getting that shape right. The outer element is <nav> with an aria-label — the label matters because most pages have more than one navigation landmark, and "navigation" repeated three times in the landmarks list tells a screen-reader user nothing. Inside it, a real <ul> of <li> and <a> gives "list, 6 items" and "link, 3 of 6" for free. Rebuild that from <div> elements and every one of those affordances disappears.
The active item is marked with aria-current="page" and nothing else. This demo styles the current item with [aria-current="page"] rather than a class, which means the visual state and the announced state cannot drift apart. The moment you keep both a class and an attribute you have two sources of truth, and the one you forget to update is the one assistive technology reads. The correct value here is page; true is for a step in a flow, and aria-selected is not applicable at all.
Expanded state belongs to the control, not to the thing it controls. A collapsible group inside a sidebar is a disclosure: the button says whether its list is open, and points at the list with aria-controls. Putting aria-expanded on the <ul> is a no-op — the attribute is only mapped on the element that owns the interaction. The pure-CSS route to this is <details> / <summary>, where the browser exposes the state itself; the scripted route is demo 05.
The trap this demo exists to head off: sidebars get built by copying a vertical tab strip. Tabs use role="tab", aria-selected and arrow-key activation, and they promise that nothing navigates. Navigation promises the opposite. Copying tab attributes onto links tells the user the wrong thing about what will happen when they activate an item, which is a correctness bug rather than a polish one — demo 04 is the boundary marker for that decision.
Make it yours
- Change the landmark name in
aria-labelto match the product area — "Documentation", "Project", "Account". It is read before the list, so make it a noun a user would recognise. - Restyle the active item by editing the
[aria-current="page"]rule. Resist adding a class alongside it; the whole point is one source of truth. - Swap the badge counts for real data, or delete
.sdb-01__countentirely — it is inside the link text so it is announced as part of the name, which is usually what you want. - Drop the
<details>group if the sidebar is flat — nothing else in the markup depends on it. - Add a second level under any group by nesting another
<ul>inside its<li>. Demo 09 covers the keyboard and hiding rules that come with it. - Replace
<details>with a scripted button andaria-expandedif the group needs to animate; the attribute contract is identical either way.
Gotchas — read before shipping
aria-selectedon a navigation link announces the tab promise — that activating it will swap a panel in place. It is the single most common wrong attribute in a sidebar and it makes the announcement actively misleading.aria-expandedon the<ul>instead of on the control does nothing at all. The attribute is only mapped on the interactive element, so the state is silently never announced.- A
<nav>with no accessible name is still a landmark, so it shows up in the landmarks list as an unlabelled "navigation" next to the header and footer ones. With three of them a user cannot tell which is the sidebar. - Styling the active item from a class while announcing it from an attribute gives you two states that can disagree. The visual one is the one you will notice in review; the announced one is the one that matters.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 15.4+ | 121+ | 111+ |
oklch() and the :has() theme toggle set the floor; the ARIA and the semantics have no version floor at all and the sidebar itself is flexbox and grid, Baseline since 2017. Engines below the oklch() floor take the sRGB hex declared first on every token and keep the operating-system colour scheme instead of the toggle, and engines without :has() lose the toggle but nothing else.