Sidebar
A composable, accessible sidebar shell for app layouts. Desktop and mobile modes, three variants (inset, floating, classic), collapsible to icon rail or off-canvas, keyboard shortcut toggle, LTR and RTL. Built on Base UI Collapsible with a full compound-component API.
Playground
Installation
pnpm add @tessinaui/uiUsage
import {
SidebarProvider,
Sidebar,
SidebarHeader,
SidebarContent,
SidebarFooter,
SidebarBrand,
SidebarSearch,
SidebarGroup,
SidebarGroupLabel,
SidebarGroupContent,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarMenuBadge,
SidebarMenuAction,
SidebarMenuSub,
SidebarMenuSubItem,
SidebarMenuSubButton,
SidebarTrigger,
SidebarInset,
SidebarSeparator,
useSidebar,
} from "@tessinaui/ui";<SidebarProvider>
<Sidebar>
<SidebarHeader>
<SidebarBrand name="Workspace" initial="W" />
<SidebarSearch placeholder="Search…" />
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton active>
<Home className="size-4" />
<span>Home</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<Inbox className="size-4" />
<span>Inbox</span>
<SidebarMenuBadge>12</SidebarMenuBadge>
</SidebarMenuButton>
<SidebarMenuAction aria-label="More">
<MoreHorizontal className="size-3.5" />
</SidebarMenuAction>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
{/* footer actions */}
</SidebarFooter>
</Sidebar>
<SidebarInset>
{/* page content */}
</SidebarInset>
</SidebarProvider>The provider sets CSS custom properties for sidebar width and tracks open/collapsed state across the tree. Wrap any layout in SidebarProvider; sibling Sidebar and SidebarInset build a two-column app shell.
Showcase
When to use
- Dashboard shells: primary navigation on the side of a content area — projects, inboxes, settings apps
- Tool palettes: persistent lists of destinations, grouped and searchable
- Editorial apps: folders, collections, pinned items with counts and submenus
If you just need a temporary overlay panel, reach for Drawer. If you need a top bar for a small-screen app, use TopHeaderMobile with a Drawer.
API Reference
SidebarProvider
Root state container. Provides sidebar context (open, openMobile, isMobile, size, dir) to the entire subtree. Also sets CSS variables (--sidebar-width, --sidebar-width-icon, --sidebar-width-mobile) on its wrapper.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | true | Initial open state on desktop |
open | boolean | — | Controlled open state |
onOpenChange | (open: boolean) => void | — | Called when open state changes |
size | "sm" | "md" | "lg" | "md" | Controls width of expanded rail |
dir | "ltr" | "rtl" | "ltr" | Text and layout direction |
style | CSSProperties | — | Merged with the provider's own CSS variables |
Also accepts any <div> attribute (className, id, etc.).
Keyboard shortcut: Cmd/Ctrl + B toggles the sidebar — on desktop it flips open, on mobile it flips openMobile.
Sidebar
The rail itself. Renders as <aside> on desktop and as a slide-in panel on mobile (via an internal backdrop + translate-x animation — no Dialog focus trap, so the sidebar stays in the page render tree).
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "sidebar" | "floating" | "inset" | "sidebar" | Visual treatment. floating detaches the panel with margin + rounded border; inset pushes content content inward inside a shared surface |
collapsible | "offcanvas" | "icon" | "none" | "offcanvas" | What "closed" means. icon collapses to a rail showing only icons; offcanvas hides the sidebar; none pins it open |
side | "left" | "right" | "left" | Which edge the sidebar sits on. Automatically flipped under dir="rtl" |
intent | "default" | "inverse" | "default" | default uses bg-background / text-foreground; inverse uses bg-foreground / text-background for a Noir-style dark rail |
State data attributes (for CSS targeting):
data-state="open" \| "collapsed"data-collapsible="icon" \| "offcanvas" \| "none"data-variant="sidebar" \| "floating" \| "inset"data-side="left" \| "right"data-intent="default" \| "inverse"data-mobile(present when rendering the mobile slide-in)
SidebarTrigger
Button that toggles the sidebar. Uses the appropriate handler based on isMobile. Accepts any <button> attribute and an onClick that runs alongside the toggle.
SidebarRail
Thin hit-area on the inner edge of the sidebar. Clicking it toggles open/collapsed. Useful when collapsible="icon" — gives the user an always-visible toggle target.
SidebarInset
Wrapper for page content that sits next to the sidebar. On desktop, reserves space using flexbox. On variant="inset", applies the shared surface border/radius.
SidebarHeader / SidebarFooter
Layout slots pinned to the top and bottom of the rail. Padded by default. Accept any <div> attribute.
SidebarBrand
Branded identity row. Shows an avatar tile with initial and a name/meta pair. Collapses gracefully to just the tile in icon mode.
| Prop | Type | Default | Description |
|---|---|---|---|
name | ReactNode | — | Primary label (workspace name) |
meta | ReactNode | — | Secondary label (plan, handle) |
initial | string | "A" | One-letter avatar tile text |
avatar | ReactNode | — | Replace the initial tile with a custom node |
action | ReactNode | — | Trailing node — commonly a chevron |
SidebarSearch
Pre-styled search field. Icon on the start, optional shortcut hint on the end. Hidden when the rail is collapsed to icons. Accepts all <input> attributes plus:
| Prop | Type | Default | Description |
|---|---|---|---|
shortcut | string | — | Right-side shortcut hint (e.g. ⌘K) |
SidebarContent
Main scroll region between header and footer. Accepts any <div> attribute.
SidebarSeparator
Thin horizontal divider. Padded on the sides to align with menu items.
SidebarGroup
Logical section. Can be collapsible.
| Prop | Type | Default | Description |
|---|---|---|---|
collapsible | boolean | false | Wrap the group in Base UI Collapsible so the label acts as a trigger |
defaultOpen | boolean | true | Initial open state of the collapsible group |
SidebarGroupLabel
Section caption. If the group is collapsible, renders as a Collapsible.Trigger <button> with a chevron. Hidden when the rail is collapsed to icons.
SidebarGroupAction
Small icon button at the end of the label row (e.g. a + action). Hidden when the rail is collapsed to icons.
SidebarGroupContent
Wrapper for group children. When the parent SidebarGroup is collapsible, wraps in Collapsible.Panel.
SidebarMenu / SidebarMenuItem
List and list-item primitives. SidebarMenu renders <ul>, SidebarMenuItem renders <li> with position: relative so badges and actions can absolutely position.
SidebarMenuButton
The clickable row. Default renders as <button>; pass render={<a ...>…} to swap in an anchor or a Next.js <Link>.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "ghost" | "pill" | "default" | Hover/active treatment |
size | "sm" | "md" | "lg" | "md" | Vertical density |
active | boolean | false | Applies the active/selected styling |
disabled | boolean | false | Dims and blocks interaction |
tooltip | string | — | Title attribute shown when collapsed to icons |
render | ReactElement | — | Swap the underlying element |
Content: place an icon (<SomeIcon className="size-4" />) and a <span> label inside. The label is auto-hidden when the rail is collapsed to icons; the icon stays visible.
SidebarMenuAction
Small icon action that floats on the end of a menu row (e.g. three-dot "more" button). Renders as a <button>, so it must be a sibling of SidebarMenuButton inside SidebarMenuItem — never a child of the button (nested <button> elements are invalid HTML). Absolutely positioned against the menu item. Hidden when the rail is collapsed to icons.
<SidebarMenuItem>
<SidebarMenuButton>…</SidebarMenuButton>
<SidebarMenuAction aria-label="More">
<MoreHorizontal className="size-3.5" />
</SidebarMenuAction>
</SidebarMenuItem>SidebarMenuBadge
Counter or status pill floated on the end of a menu row. Renders as a <span> and uses margin-inline-start: auto to push to the end of its flex container — so it belongs inside SidebarMenuButton (which is the flex row). Accepts any <span> attribute plus:
| Prop | Type | Default | Description |
|---|---|---|---|
tone | "neutral" | "primary" | "success" | "warning" | "error" | "info" | "neutral" | Maps to Tessina intent tokens (bg-*-light / text-*) |
<SidebarMenuItem>
<SidebarMenuButton>
<Inbox className="size-4" />
<span>Inbox</span>
<SidebarMenuBadge>12</SidebarMenuBadge>
</SidebarMenuButton>
</SidebarMenuItem>Hidden when the rail is collapsed to icons.
SidebarMenuSub / SidebarMenuSubItem / SidebarMenuSubButton
Nested tree of links under a parent SidebarMenuItem. SidebarMenuSub renders <ul> with a start border to hint the tree; SidebarMenuSubButton behaves like SidebarMenuButton but slightly denser and without icon slot rules.
useSidebar()
React hook for consuming the sidebar context.
const { open, setOpen, openMobile, setOpenMobile, toggle, isMobile, size, dir } = useSidebar();Throws if called outside a SidebarProvider.
Variants
| Variant | When to use |
|---|---|
sidebar | Classic app shell — flush against the edge, shares a border with content |
floating | Detached card look — sits on an off-white surface with a soft border and rounded corners |
inset | Modern dashboard — sidebar and content share a unified surface with inner padding |
Collapsing behaviour
collapsible | Closed state |
|---|---|
offcanvas | Slides fully out of view; toggle brings it back |
icon | Shrinks to an icon rail (--sidebar-width-icon); labels hide, icons stay |
none | Always open — no toggle behaviour |
RTL
Pass dir="rtl" on SidebarProvider. The sidebar flips sides automatically, chevrons and badge positions mirror, and logical CSS (border-s, ms-auto, ps-) keeps paddings consistent. All compound parts respect the direction via context.
Accessibility
- Navigation is a semantic
<nav>witharia-label="Sidebar". - Collapsible groups use Base UI
Collapsible— the label is a real<button>witharia-expanded. - The rail target is a
<button>witharia-label="Toggle Sidebar". - Mobile overlay is dismissible via backdrop click or the
Escapekey; focus returns to the trigger on close. - All menu buttons respond to
Space/Enterand receive a visible focus ring (focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2). Cmd/Ctrl + Btoggles the sidebar globally.