Pure CSSMIT licensed

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.

Live Demo
Plain CSSTailwind v4
Try it

Mosaic by Cruip: a complete Tailwind admin dashboard template in HTML, React, Next.js and Vue · $69

Cruip · Mosaic by Cruip · Affiliate link

View Mosaic dashboard

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>
.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;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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-label to 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__count entirely — 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 and aria-expanded if the group needs to animate; the attribute contract is identical either way.

Gotchas — read before shipping

  • aria-selected on 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-expanded on 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

ChromeSafariFirefoxEdge
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.

Techniques used in this demo

Search CodeFronts

Loading…