Skip to content

Documentation

neelam-ui

Search documentation

Getting Started
Forms
Overlays
Navigation
Data Display
Layout
AI & Chat
Blocks
GitHub repository

Sidebar

A docked, collapsible navigation panel and the flex layout that holds it beside your main content.

Overview

Active users
12.9KIncreased by+12.4%
Open invoices
$18.2KDecreased by-4.3%vs. last month

Recent activity

  • Grace Hoppermerged “Billing rewrite”12m ago
  • Alan Turingcommented on INV-2041h ago
  • Ada Lovelaceinvited 3 teammates3h ago

Usage#

import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from "neelam-ui";
 
<SidebarProvider>
  <Sidebar>
    <SidebarHeader>Acme</SidebarHeader>
    <SidebarContent>
      <SidebarGroup>
        <SidebarGroupLabel>Workspace</SidebarGroupLabel>
        <SidebarMenu>
          <SidebarMenuItem>
            <SidebarMenuButton href="/" isActive>Overview</SidebarMenuButton>
          </SidebarMenuItem>
        </SidebarMenu>
      </SidebarGroup>
    </SidebarContent>
  </Sidebar>
  <main className="flex-1">
    <SidebarTrigger />
  </main>
</SidebarProvider>

SidebarProvider renders the shared flex row itself, rather than leaving it to you. Unlike Dialog — whose provider holds only context, because the content portals into the top layer — a docked sidebar and its main content are ordinary flex siblings that need a wrapper.

Collapsing#

open and defaultOpen control it, and useSidebar() exposes the state plus a setter and toggle for a control SidebarTrigger does not cover.

defaultOpen is true: unlike a dialog, a docked sidebar is normally visible from the start.

Collapsing animates the <aside>'s own width. Its children sit in an inner <div> held at a constant width instead of collapsing with it, so header and nav text does not visibly reflow and wrap mid-animation as the outer width crosses toward zero.

Collapsed means inert, not just invisible

A collapsed sidebar is marked inert, so it is unreachable by keyboard and removed from the accessibility tree while still present for the closing transition. Without that, Tab would walk through invisible links — the same problem Dialog and Drawer solve by inerting the page behind them.

Collapsing to an icon rail#

By default (collapsible="offcanvas"), closing collapses the panel all the way to a hidden, inert 0-width panel — the behaviour above. Pass collapsible="icon" and it collapses to a slim, still-visible-and-interactive rail instead:

Overview

Collapse the sidebar — it becomes an icon rail, not a hidden panel. Hover a link to see its label again.
<Sidebar collapsible="icon">…</Sidebar>

Because the rail stays interactive, it's no longer inert while collapsed — unlike the offcanvas default, there's nothing invisible here for Tab to skip. Each SidebarMenuButton's label isn't dropped, either: it's kept sr-only (so the link's accessible name is unchanged) and resurfaces as a Tooltip on hover or focus, wrapping the <a> itself via TooltipTrigger's asChild rather than an extra focusable wrapper around it. SidebarGroupLabel renders nothing at all while collapsed — a rail that narrow has no room for a text heading, and unlike a menu button there's no icon standing in for it to attach a tooltip to instead.

iconWidth (default "4rem") sets the rail's width, the same way width sets the open one. It's deliberately roomier than a single icon looks like it needs — SidebarContent's own p-3 padding still applies inside the rail, so a tighter width leaves the icon's h-8 w-8 box no space to actually center in:

<Sidebar collapsible="icon" iconWidth="4.5rem" />

Header and footer content is yours to adapt

Unlike a menu button's icon+label shape, SidebarHeader/SidebarFooter content has no single common shape this component could restyle for you. The demo above reads open from useSidebar() — the same hook a custom trigger would use — to hide its header/footer text while collapsed.

Width#

width is a CSS length (default "16rem") applied as an inline style, not a Tailwind w-* class. That is deliberate: this library hit a real bug in Resizable where a component's own default class silently beat a caller's override of the same property, because which of two equal-specificity classes wins is decided by their order in the generated stylesheet, not by anything in the className string. An inline style has no such ambiguity.

So change the width with the width prop rather than by fighting a class:

<Sidebar width="20rem" />

Marking the current page#

SidebarMenuButton renders a native <a> and takes isActive, which sets aria-current="page" alongside the highlight. Pass icon for a leading glyph — it is placed and hidden from assistive tech for you.

Keyboard#

Keyboard shortcuts
KeyBehaviour
TabMoves through the sidebar's links in order. With the default collapsible="offcanvas", a collapsed sidebar is inert and skipped entirely; with collapsible="icon" it stays reachable, and Tab shows each link's Tooltip label as it's focused.
EnterFollows the focused link. Native anchor behaviour.
EnterSpaceToggles the sidebar when focus is on SidebarTrigger.

There is no arrow-key roving. A sidebar is a list of ordinary links inside a landmark, not a composite widget, so each link is its own tab stop and browser find-in-page works normally.

Accessibility#

  • Sidebar renders a native <aside>, whose implicit complementary landmark needs no role — the same reasoning Breadcrumb renders a plain <nav>.
  • SidebarMenu is a real <ul> of <li>s, so assistive tech announces how many items there are and which one you are on.
  • The current page carries aria-current="page", not just a colour.
  • SidebarTrigger is an icon-only button with a default aria-label="Toggle sidebar" — override it if that does not fit.
  • With the default collapsible="offcanvas", the collapsed panel is inert, so it never becomes a set of invisible tab stops. With collapsible="icon", it deliberately isn't — the rail is real, visible content, so it stays in the accessibility tree, and each link's accessible name is unchanged (its label stays sr-only, not removed) even though the rail is too narrow to show it.

Two landmarks, not one

<aside> marks the sidebar; give your main region a real <main>, as the usage snippet does. Landmark navigation is how many screen reader users move around an app, and it only works if both halves are marked.

API reference#

SidebarProvider#

Props for SidebarProvider
PropTypeDefault
defaultOpen

Initial open state when uncontrolled. Defaults to `true` — unlike `Dialog`, a docked sidebar is normally visible from the start.

booleantrue
onOpenChange

Called whenever the open state changes, whether from `SidebarTrigger` or `useSidebar()`.

((open: boolean) => void)
open

Controls the open state. Omit to let the sidebar manage its own state.

boolean
Props for Sidebar
PropTypeDefault
collapsible

How the panel behaves while closed. `"offcanvas"` (the default) is the original behavior: collapse all the way to a hidden `0`-width panel, `inert` and out of the accessibility tree. `"icon"` collapses to a slim, still-visible-and-interactive icon rail instead — see `iconWidth`, and `SidebarMenuButton`/`SidebarGroupLabel` for how their content adapts to it.

enumoffcanvas
iconWidth

The panel's width while collapsed with `collapsible="icon"`. Ignored (and unused) for `"offcanvas"`, which always collapses to `0`. Defaults to `"4rem"` — deliberately roomier than it might look needed for a single `h-8 w-8` icon: `SidebarContent`'s own `p-3` padding still applies inside the rail, and the default leaves real breathing room either side of the icon rather than a bare, exact fit.

string4rem
side

Which edge the sidebar is docked to. Defaults to `"left"`.

enumleft
width

The sidebar's width while open, as a CSS length. Defaults to `"16rem"`.

string16rem

SidebarMenuButton#

Props for SidebarMenuButton
PropTypeDefault
icon

An icon shown before the label.

ReactNode
isActive

Marks this as the current page: sets `aria-current="page"` and highlights it.

booleanfalse