Sidebar

A composable, themeable and customizable sidebar component.

Usage

HTML + JavaScript

Step 1: Include the JavaScript files

You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:

<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1.0.0/dist/js/sidebar.min.js" defer></script>

Step 2: Add your sidebar HTML

<aside class="sidebar" data-side="left" aria-hidden="false">
  <nav aria-label="Sidebar navigation">
    <section class="scrollbar">
      <div role="group" aria-labelledby="group-label-content-1">
        <h3 id="group-label-content-1">Getting started</h3>

        <ul>
          <li>
            <a href="#">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="m7 11 2-2-2-2" />
                <path d="M11 13h4" />
                <rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
              </svg>
              <span>Playground</span>
            </a>
          </li>

          <li>
            <a href="#">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M12 8V4H8" />
                <rect width="16" height="12" x="4" y="8" rx="2" />
                <path d="M2 14h2" />
                <path d="M20 14h2" />
                <path d="M15 13v2" />
                <path d="M9 13v2" />
              </svg>
              <span>Models</span>
            </a>
          </li>

          <li>
            <details id="submenu-content-1-3">
              <summary aria-controls="submenu-content-1-3-content">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                  <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
                  <circle cx="12" cy="12" r="3" />
                </svg>
                <span>Settings</span>
              </summary>
              <ul id="submenu-content-1-3-content">
                <li>
                  <a href="#">
                    <span>General</span>
                  </a>
                </li>

                <li>
                  <a href="#">
                    <span>Team</span>
                  </a>
                </li>

                <li>
                  <a href="#">
                    <span>Billing</span>
                  </a>
                </li>

                <li>
                  <a href="#">
                    <span>Limits</span>
                  </a>
                </li>
              </ul>
            </details>
          </li>
        </ul>
      </div>
    </section>
  </nav>
</aside>

<main>
  <button type="button" onclick="document.getElementById('sidebar')?.toggle()">Toggle sidebar</button>
  <h1>Content</h1>
</main>

HTML structure

<aside class="sidebar" aria-hidden="false">
Wraps the sidebar component. Supported attributes:
  • id: target a specific sidebar from JavaScript methods.
  • aria-hidden: current open state. false means visible, true means hidden.
  • data-side: physical side of the viewport. Use left or right. Defaults to left.
  • data-initial-open: set to false to start closed on desktop.
  • data-initial-mobile-open: set to true to start open below the breakpoint.
  • data-breakpoint: pixel width used to switch between desktop and mobile behavior. Defaults to 768.
<nav aria-label="...">
The semantic navigation landmark inside the sidebar.
<header> Optional
Top area for branding, workspace switchers, or primary controls.
<section>
Scrollable sidebar content.
<div role="group" aria-labelledby="...">
A grouped set of navigation items. Use an h3 with a matching id when the group has a visible label.
<ul> / <li>
Navigation list structure. Items may contain an a, a button, or a details submenu.
<a aria-current="page">
Use aria-current="page" for the active link. You can also use data-active="true" for non-link active items.
<button type="button">
Use buttons for actions. On mobile, links and buttons close the sidebar unless they include data-keep-mobile-sidebar-open.
<details> / <summary>
Use native disclosure for submenus. The nested ul contains the submenu items.
<footer> Optional
Bottom area for account controls, settings, or secondary actions.
<main>
The sibling content wrapper. Basecoat applies the desktop margin to the sibling that follows the sidebar.
<button type="button" onclick="document.getElementById('sidebar')?.toggle()">
Calls the sidebar method used to toggle, open, or close the sidebar.
<!-- Server-side or router-driven current item -->
<nav aria-label="Sidebar navigation">
  <ul>
    <li><a href="/my-path?page=overview">Overview</a></li>
    <li><a href="/my-path?page=settings" aria-current="page">Settings</a></li>
    <li><button type="button" data-active="true">Active action</button></li>
  </ul>
</nav>

Basecoat differences

Basecoat keeps sidebar intentionally smaller than shadcn/ui. It supports fixed left/right sidebars, mobile overlay behavior, grouped navigation, active states, and native details submenus.

It does not currently expose shadcn/ui's React provider, rail, inset/floating variants, icon-only collapse mode, menu actions, menu badges, or menu skeleton API. Those can be composed manually if needed, but they are not part of the Basecoat sidebar contract.

RTL content is supported through logical spacing and borders. The data-side value remains physical: left means the left side of the viewport and right means the right side.

JavaScript API

basecoat:initialized
Once the component is fully initialized, it dispatches a custom (non-bubbling) basecoat:initialized event on itself.
sidebar.open()
Opens the specific sidebar element.
sidebar.close()
Closes the specific sidebar element.
sidebar.toggle()
Toggles the specific sidebar element.
<!-- Toggles the sidebar -->
<button type="button" onclick="document.getElementById('sidebar')?.toggle()">Toggle sidebar</button>
<!-- Opens the `#main-navigation` sidebar -->
<button type="button" onclick="document.getElementById('main-navigation')?.open()">Open sidebar</button>
<!-- Closes the sidebar -->
<button type="button" onclick="document.getElementById('sidebar')?.close()">Close sidebar</button>

Jinja and Nunjucks

You can use the sidebar() Nunjucks or Jinja macro for this component.

{% set menu = [
  { type: "group", label: "Getting started", items: [
    { label: "Playground", url: "#" },
    { label: "Models", url: "#" },
    { label: "Settings", type: "submenu", items: [
      { label: "General", url: "#" },
      { label: "Team", url: "#" },
      { label: "Billing", url: "#" },
      { label: "Limits", url: "#" }
    ] }
  ]}
] %}

{{ sidebar(
  label="Sidebar navigation",
  menu=menu
) }}
<main>
  <h1>Content</h1>
</main>