Open demo

CSS code

HTML
<aside class="sbm">
  <div class="sbm-logo"><i aria-hidden="true"></i>Console</div>
  <nav aria-label="Console">
    <p class="sbm-group" id="sbm-main">Main</p>
    <ul aria-labelledby="sbm-main">
      <li><a class="sbm-item" href="#" aria-current="page"><span aria-hidden="true">📊</span> Dashboard</a></li>
      <li><a class="sbm-item" href="#"><span aria-hidden="true">📁</span> Projects</a></li>
      <li>
        <button class="sbm-item" type="button" aria-expanded="true" aria-controls="sbm-resources"><span aria-hidden="true">🛠️</span> Resources <span class="sbm-chev" aria-hidden="true">▸</span></button>
        <ul class="sbm-subs" id="sbm-resources">
          <li><a href="#">Databases</a></li>
          <li><a href="#">Storage</a></li>
          <li><a href="#">Functions</a></li>
        </ul>
      </li>
    </ul>
    <p class="sbm-group" id="sbm-workspace">Workspace</p>
    <ul aria-labelledby="sbm-workspace">
      <li><a class="sbm-item" href="#"><span aria-hidden="true">👥</span> Team</a></li>
      <li><a class="sbm-item" href="#"><span aria-hidden="true">⚙️</span> Settings</a></li>
    </ul>
  </nav>
</aside>
CSS
/* Sidebar navigation: grouped link lists, aria-current on the active page and a disclosure button that expands a submenu in place. */
.sbm{width:250px;flex:none;background:#0f1523;color:#c6cede;padding:20px 14px;display:flex;flex-direction:column;min-height:100vh}
.sbm ul{list-style:none;margin:0;padding:0}
.sbm-logo{display:flex;align-items:center;gap:10px;font-weight:800;color:#fff;padding:6px 12px 22px}
.sbm-logo i{width:26px;height:26px;border-radius:8px;background:linear-gradient(135deg,#22d3ee,#818cf8)}
.sbm-group{margin:0;font-size:.66rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:#8a96b3;padding:14px 12px 6px}
.sbm-item{display:flex;align-items:center;gap:11px;width:100%;padding:10px 12px;border-radius:9px;font:inherit;font-size:.87rem;font-weight:600;line-height:normal;color:#9aa5be;text-decoration:none;border:0;background:none;cursor:pointer;transition:background-color .13s,color .13s;text-align:left}
.sbm-item:hover{background:#1a2233;color:#e7ecf7}
.sbm-item[aria-current="page"]{background:#1e293e;color:#fff;box-shadow:inset 3px 0 0 #22d3ee}
.sbm-chev{margin-left:auto;transition:transform .2s;font-size:.7rem}
.sbm-item[aria-expanded="true"] .sbm-chev{transform:rotate(90deg)}
.sbm-subs{max-height:0;overflow:hidden;visibility:hidden;transition:max-height .25s ease,visibility .25s}
.sbm-item[aria-expanded="true"]+.sbm-subs{max-height:200px;visibility:visible}
.sbm-subs a{display:block;padding:8px 12px 8px 43px;font-size:.83rem;color:#8791ab;text-decoration:none;border-radius:8px}
.sbm-subs a:hover{color:#e7ecf7;background:#1a2233}
.sbm a:focus-visible,.sbm button:focus-visible{outline:2px solid #22d3ee;outline-offset:-2px}
@media (max-width:640px){.sbm{width:100%;min-height:0}}
@media (prefers-reduced-motion:reduce){.sbm-item,.sbm-chev,.sbm-subs{transition:none}}
JavaScript
/* Each submenu button flips aria-expanded; CSS expands the list after it. Esc inside an open submenu closes it and returns focus to its button. */
document.querySelectorAll('.sbm button[aria-controls]').forEach(btn => {
  const list = document.getElementById(btn.getAttribute('aria-controls'));
  btn.addEventListener('click', () => btn.setAttribute('aria-expanded', btn.getAttribute('aria-expanded') !== 'true'));
  list.addEventListener('keydown', e => { if (e.key === 'Escape') { btn.setAttribute('aria-expanded', 'false'); btn.focus(); } });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<aside class="sbm">
  <div class="sbm-logo"><i aria-hidden="true"></i>Console</div>
  <nav aria-label="Console">
    <p class="sbm-group" id="sbm-main">Main</p>
    <ul class="nav flex-column" aria-labelledby="sbm-main">
      <li class="nav-item"><a class="nav-link active" href="#" aria-current="page"><span aria-hidden="true">📊</span> Dashboard</a></li>
      <li class="nav-item"><a class="nav-link" href="#"><span aria-hidden="true">📁</span> Projects</a></li>
      <li class="nav-item">
        <button class="nav-link" type="button" data-bs-toggle="collapse" data-bs-target="#sbm-resources" aria-expanded="true" aria-controls="sbm-resources"><span aria-hidden="true">🛠️</span> Resources <span class="sbm-chev" aria-hidden="true">▸</span></button>
        <ul class="nav flex-column collapse show" id="sbm-resources">
          <li class="nav-item"><a class="nav-link" href="#">Databases</a></li>
          <li class="nav-item"><a class="nav-link" href="#">Storage</a></li>
          <li class="nav-item"><a class="nav-link" href="#">Functions</a></li>
        </ul>
      </li>
    </ul>
    <p class="sbm-group" id="sbm-workspace">Workspace</p>
    <ul class="nav flex-column" aria-labelledby="sbm-workspace">
      <li class="nav-item"><a class="nav-link" href="#"><span aria-hidden="true">👥</span> Team</a></li>
      <li class="nav-item"><a class="nav-link" href="#"><span aria-hidden="true">⚙️</span> Settings</a></li>
    </ul>
  </nav>
</aside>
CSS
/* Bootstrap .nav.flex-column in a dark sidebar. The submenu is a .collapse driven by the Collapse plugin,
   which keeps aria-expanded on the button in sync; a few lines of script add Esc to close. */
.sbm .nav{--bs-nav-link-padding-x:12px;--bs-nav-link-padding-y:10px;--bs-nav-link-font-size:.87rem;--bs-nav-link-font-weight:600;--bs-nav-link-color:#9aa5be;--bs-nav-link-hover-color:#e7ecf7}
.sbm{width:250px;flex:none;background:#0f1523;color:#c6cede;padding:20px 14px;display:flex;flex-direction:column;min-height:100vh}
.sbm-logo{display:flex;align-items:center;gap:10px;font-weight:800;color:#fff;padding:6px 12px 22px}
.sbm-logo i{width:26px;height:26px;border-radius:8px;background:linear-gradient(135deg,#22d3ee,#818cf8)}
.sbm-group{margin:0;font-size:.66rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:#8a96b3;padding:14px 12px 6px}
.sbm .nav-link{display:flex;align-items:center;gap:11px;width:100%;border-radius:9px;line-height:normal;text-align:left;transition:background-color .13s,color .13s}
.sbm .nav-link:hover{background:#1a2233}
.sbm .nav-link.active{background:#1e293e;color:#fff;box-shadow:inset 3px 0 0 #22d3ee}
.sbm-chev{margin-left:auto;transition:transform .2s;font-size:.7rem}
.sbm .nav-link[aria-expanded="true"] .sbm-chev{transform:rotate(90deg)}
.sbm .collapse .nav-link{display:block;padding:8px 12px 8px 43px;font-size:.83rem;font-weight:400;color:#8791ab;border-radius:8px}
.sbm .collapse .nav-link:hover{color:#e7ecf7}
.sbm .nav-link:focus-visible{outline:2px solid #22d3ee;outline-offset:-2px;box-shadow:none}
@media (max-width:640px){.sbm{width:100%;min-height:0}}
@media (prefers-reduced-motion:reduce){.sbm .nav-link,.sbm-chev,.sbm .collapsing{transition:none}}
JavaScript
/* Esc inside an open submenu collapses it and returns focus to its button. */
document.querySelectorAll('.sbm .collapse').forEach(list => {
  const btn = document.querySelector(`[data-bs-target="#${list.id}"]`);
  list.addEventListener('keydown', e => { if (e.key === 'Escape') { bootstrap.Collapse.getOrCreateInstance(list, { toggle: false }).hide(); btn.focus(); } });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<aside class="sbm flex min-h-screen w-[250px] flex-none flex-col bg-sbm-bg px-3.5 py-5 text-[#c6cede] max-[640px]:min-h-0 max-[640px]:w-full">
  <div class="flex items-center gap-2.5 px-3 pt-1.5 pb-[22px] font-extrabold text-white"><i class="size-[26px] rounded-lg bg-linear-135/srgb from-[#22d3ee] to-[#818cf8]" aria-hidden="true"></i>Console</div>
  <nav aria-label="Console">
    <p class="px-3 pt-3.5 pb-1.5 text-[.66rem] font-extrabold tracking-[.1em] text-[#8a96b3] uppercase" id="sbm-main">Main</p>
    <ul aria-labelledby="sbm-main">
      <li><a class="flex w-full items-center gap-[11px] rounded-[9px] bg-sbm-active px-3 py-2.5 text-[.87rem] font-semibold text-white shadow-[inset_3px_0_0_var(--color-sbm-accent)] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sbm-accent" href="#" aria-current="page"><span aria-hidden="true">📊</span> Dashboard</a></li>
      <li><a class="flex w-full items-center gap-[11px] rounded-[9px] px-3 py-2.5 text-[.87rem] font-semibold text-sbm-link transition-colors duration-[130ms] hover:bg-sbm-hover hover:text-[#e7ecf7] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sbm-accent motion-reduce:transition-none" href="#"><span aria-hidden="true">📁</span> Projects</a></li>
      <li>
        <button class="group peer flex w-full cursor-pointer items-center gap-[11px] rounded-[9px] px-3 py-2.5 text-left text-[.87rem] font-semibold text-sbm-link transition-colors duration-[130ms] hover:bg-sbm-hover hover:text-[#e7ecf7] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sbm-accent motion-reduce:transition-none" type="button" aria-expanded="true" aria-controls="sbm-resources"><span aria-hidden="true">🛠️</span> Resources <span class="ml-auto text-[.7rem] transition-transform duration-200 group-aria-expanded:rotate-90 motion-reduce:transition-none" aria-hidden="true">▸</span></button>
        <ul class="invisible max-h-0 overflow-hidden transition-[max-height,visibility] duration-[250ms] ease-in-out peer-aria-expanded:visible peer-aria-expanded:max-h-[200px] motion-reduce:transition-none" id="sbm-resources">
          <li><a class="block rounded-lg py-2 pr-3 pl-[43px] text-[.83rem] text-[#8791ab] hover:bg-sbm-hover hover:text-[#e7ecf7] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sbm-accent" href="#">Databases</a></li>
          <li><a class="block rounded-lg py-2 pr-3 pl-[43px] text-[.83rem] text-[#8791ab] hover:bg-sbm-hover hover:text-[#e7ecf7] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sbm-accent" href="#">Storage</a></li>
          <li><a class="block rounded-lg py-2 pr-3 pl-[43px] text-[.83rem] text-[#8791ab] hover:bg-sbm-hover hover:text-[#e7ecf7] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sbm-accent" href="#">Functions</a></li>
        </ul>
      </li>
    </ul>
    <p class="px-3 pt-3.5 pb-1.5 text-[.66rem] font-extrabold tracking-[.1em] text-[#8a96b3] uppercase" id="sbm-workspace">Workspace</p>
    <ul aria-labelledby="sbm-workspace">
      <li><a class="flex w-full items-center gap-[11px] rounded-[9px] px-3 py-2.5 text-[.87rem] font-semibold text-sbm-link transition-colors duration-[130ms] hover:bg-sbm-hover hover:text-[#e7ecf7] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sbm-accent motion-reduce:transition-none" href="#"><span aria-hidden="true">👥</span> Team</a></li>
      <li><a class="flex w-full items-center gap-[11px] rounded-[9px] px-3 py-2.5 text-[.87rem] font-semibold text-sbm-link transition-colors duration-[130ms] hover:bg-sbm-hover hover:text-[#e7ecf7] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sbm-accent motion-reduce:transition-none" href="#"><span aria-hidden="true">⚙️</span> Settings</a></li>
    </ul>
  </nav>
</aside>
CSS
@theme {
  --color-sbm-bg: #0f1523;
  --color-sbm-hover: #1a2233;
  --color-sbm-active: #1e293e;
  --color-sbm-link: #9aa5be;
  --color-sbm-accent: #22d3ee;
}
JavaScript
/* Each submenu button flips aria-expanded; peer-aria-expanded: utilities expand the list after it.
   Esc inside an open submenu closes it and returns focus to its button. */
document.querySelectorAll('.sbm button[aria-controls]').forEach(btn => {
  const list = document.getElementById(btn.getAttribute('aria-controls'));
  btn.addEventListener('click', () => btn.setAttribute('aria-expanded', btn.getAttribute('aria-expanded') !== 'true'));
  list.addEventListener('keydown', e => { if (e.key === 'Escape') { btn.setAttribute('aria-expanded', 'false'); btn.focus(); } });
});

About this set

A dark 250 pixel sidebar for dashboards and admin panels. It has a logo row, two labelled groups (Main and Workspace), an active Dashboard item marked by a cyan spine and a lighter background, and a Resources item that expands in place to show Databases, Storage and Functions. Use it as the permanent left column of an app where the section list is short enough to stay visible. Each group is a list labelled by its small uppercase heading, and the active link carries aria-current=”page”. The Resources entry is a disclosure button: it flips aria-expanded, CSS rotates its chevron and expands the list below it by animating max-height, and the collapsed list is hidden from the tab order so keyboard users never land on invisible links. Below 640 pixels the sidebar stops being a column and stacks above the page content at full width. The Bootstrap version uses .nav.flex-column with the Collapse plugin for the submenu, and the Tailwind version drives the same states with peer-aria-expanded utilities.

What’s included

  • Grouped links under Main and Workspace headings
  • Active item with a cyan accent spine and aria-current
  • Submenu that expands in place from a disclosure button
  • Collapsed links leave the tab order
  • Stacks above the content below 640px

Accessibility

The sidebar links sit in a nav labelled Console, and each group list is named by its visible heading. The Resources button reports its state with aria-expanded, opens with Enter or Space, and Esc inside the open submenu closes it and returns focus to the button. Every link and button shows a 2px cyan outline on keyboard focus, and the expand animation is removed under prefers-reduced-motion.

Customise it

Change the sidebar background, link colour and accent in .sbm, .sbm-item and the inset box-shadow (plain CSS), the --bs-nav-link-* variables on .sbm .nav (Bootstrap) or the --color-sbm-* theme colours (Tailwind). The width is the 250px on .sbm or w-[250px].