Sidebar
The application shell sidebar — a composable part-family ported from shadcn’s sidebar. One SidebarProvider owns the open/collapsed state (and persists it to a cookie); everything else is layout you assemble: a Sidebar container, a SidebarInset for the main content, and the SidebarHeader / SidebarContent / SidebarFooter / SidebarGroup / SidebarMenu slots in between.
This is what web/wms renders for its whole app chrome (collapsible="icon", variant="inset").
The Provider is required
Every part reads state through useSidebar(), which throws outside a SidebarProvider. Wrap the whole layout — both the Sidebar and the SidebarInset — in one provider, normally at the root of your app so the cookie-restored state applies before first paint.
import { SidebarProvider, Sidebar, SidebarInset } from "@vesyl/ui-next";
<SidebarProvider>
<Sidebar>{/* header / content / footer */}</Sidebar>
<SidebarInset>{/* page */}</SidebarInset>
</SidebarProvider>;The provider renders a full-height (min-h-svh), full-width flex wrapper and supplies the --sidebar-width (16rem) and --sidebar-width-icon (3rem) CSS variables. It also mounts a TooltipProvider, so SidebarMenuButton tooltips work without extra setup. The demos on this page override the height to a fixed 420px box so they fit inside the preview — in a real app you let it fill the viewport.
State & persistence
SidebarProvideris uncontrolled by default (defaultOpen, defaulttrue). Passopen+onOpenChangeto drive it yourself.- Toggling writes a
sidebar_statecookie (7-day max-age), so the open/collapsed choice survives reloads. SSR can read that cookie and passdefaultOpento avoid a flash. - A global keyboard shortcut — ⌘/Ctrl + B — toggles the sidebar.
- Below 768px the desktop sidebar is replaced by a
Sheetdrawer;open/setOpendrive desktop,openMobile/setOpenMobiledrive the drawer.toggleSidebar()picks the right one.
Composition
Assemble the parts inside Sidebar. The common shape is a SidebarHeader (branding / org switcher), a scrollable SidebarContent holding one or more SidebarGroups, and a SidebarFooter (user menu, support). Inside a group, a SidebarMenu (<ul>) holds SidebarMenuItems (<li>), each wrapping a SidebarMenuButton. Drop a lucide icon plus a <span> label inside the button.
SidebarTrigger is a ghost icon button that calls toggleSidebar() — drop it in your page header (inside SidebarInset) so users can collapse the rail. SidebarInset is the <main> region beside the sidebar; it grows to fill the remaining width and, under variant="inset", gets the rounded, inset card treatment.
Linking menu buttons
SidebarMenuButton (and SidebarMenuSubButton) render a <button> by default. To make one a router link, pass render with your framework’s Link — the part merges its classes onto your element via Base UI’s useRender, so the typed Link is never erased. Mark the current route with isActive (sets data-active, which drives the active styling).
import { Link } from "@tanstack/react-router";
<SidebarMenuButton isActive={isActive} render={<Link to="/orders" />}>
<Inbox />
<span>Orders</span>
</SidebarMenuButton>;Collapsible to icons
collapsible="icon" (vs. the default "offcanvas", or "none") keeps the rail visible when collapsed, narrowing it to --sidebar-width-icon so only the leading icons show. Pair each SidebarMenuButton with a tooltip prop — the label surfaces as a right-side tooltip only while collapsed (and never on mobile). Add a SidebarRail for the thin click-to-toggle strip on the sidebar’s edge.
In icon mode the SidebarGroupLabel, SidebarMenuSub, SidebarMenuAction, and SidebarMenuBadge parts hide themselves automatically, and SidebarMenuButton collapses to a square. Note that collapsible="icon" needs the desktop (peer-positioned, absolutely placed) render path — at viewports under 768px the sidebar always falls back to the mobile Sheet drawer regardless of this prop.
Nested menus, badges & actions
A menu item can carry more than a button. Wrap the item in a Collapsible and nest a SidebarMenuSub (with SidebarMenuSubItem / SidebarMenuSubButton) for a second level. SidebarMenuBadge pins a count to the trailing edge, and SidebarMenuAction adds a secondary control (here the collapse chevron) — pass showOnHover to keep it hidden until the row is hovered or focused.
Variants & sides
Sidebar takes three layout knobs:
variant—"sidebar"(default, flush to the edge),"floating"(detached, rounded, bordered card), or"inset"(the sidebar sits in the page margin and theSidebarInsetbecomes a rounded inset card).web/wmsuses"inset".side—"left"(default) or"right".collapsible—"offcanvas"(default; slides fully off-screen),"icon"(collapses to the icon rail), or"none"(a static, non-collapsing column — what the demos above use so they render predictably in a small box).
Other parts
A few slots aren’t shown in the demos above but are part of the family:
SidebarInput— a sidebar-styledInputfor an in-rail search/filter box; place it in theSidebarHeader.SidebarSeparator— a sidebar-tunedSeparator(correct color + inset margins).SidebarGroupAction— an action button pinned to aSidebarGroupLabel(e.g. a ”+” to add to that group).SidebarMenuSkeleton— a loading placeholder row (optionalshowIcon) for menus that hydrate from data.
// Loading state for a data-backed menu
<SidebarMenu>
{Array.from({ length: 5 }).map((_, i) => (
<SidebarMenuItem key={i}>
<SidebarMenuSkeleton showIcon />
</SidebarMenuItem>
))}
</SidebarMenu>Reference
Parts
| Part | Element | Description |
|---|---|---|
SidebarProvider | div | Owns state + cookie; supplies CSS vars and a TooltipProvider. Required wrapper. |
Sidebar | div | The sidebar container. Props: side, variant, collapsible |
SidebarInset | main | The main content region beside the sidebar; rounds into a card under variant="inset" |
SidebarTrigger | Button | Ghost icon button that calls toggleSidebar() |
SidebarRail | button | Thin click-to-toggle strip on the sidebar’s edge |
SidebarHeader | div | Top slot (branding, switchers) |
SidebarContent | div | Scrollable middle slot; holds groups |
SidebarFooter | div | Bottom slot (user menu, support) |
SidebarSeparator | Separator | Sidebar-tuned divider |
SidebarInput | Input | Sidebar-styled input for an in-rail search box |
SidebarGroup | div | A labeled section of the content |
SidebarGroupLabel | div* | Group heading; hides in icon mode |
SidebarGroupAction | button* | Action pinned to a group label |
SidebarGroupContent | div | Wrapper for a group’s body |
SidebarMenu | ul | List of menu items |
SidebarMenuItem | li | One menu row |
SidebarMenuButton | button* | The clickable row. Props: isActive, variant, size, tooltip |
SidebarMenuAction | button* | Secondary control on a row. Prop: showOnHover |
SidebarMenuBadge | div | Trailing count/badge on a row |
SidebarMenuSkeleton | div | Loading placeholder row. Prop: showIcon |
SidebarMenuSub | ul | Nested sub-menu list; hides in icon mode |
SidebarMenuSubItem | li | One sub-menu row |
SidebarMenuSubButton | a* | The clickable sub-row. Props: isActive, size |
* Parts marked with an asterisk accept a render prop (Base UI useRender) to swap the underlying element — e.g. a router Link.
SidebarProvider
Extends React.ComponentProps<'div'>.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | true | Initial open state (uncontrolled) |
open | boolean | — | Controlled open state |
onOpenChange | (open: boolean) => void | — | Fires when open state changes (controlled) |
className | string | — | Merged onto the wrapper |
style | CSSProperties | — | Merged after the --sidebar-width* vars |
Sidebar
Extends React.ComponentProps<'div'>.
| Prop | Type | Default | Description |
|---|---|---|---|
side | "left" | "right" | "left" | Which edge the sidebar docks to |
variant | "sidebar" | "floating" | "inset" | "sidebar" | Flush, detached card, or inset (page-margin) treatment |
collapsible | "offcanvas" | "icon" | "none" | "offcanvas" | Slide off-screen, collapse to an icon rail, or never collapse |
SidebarMenuButton
Extends React.ComponentProps<'button'> and Base UI useRender.ComponentProps<'button'>.
| Prop | Type | Default | Description |
|---|---|---|---|
isActive | boolean | false | Marks the current route; sets data-active and the active styling |
variant | "default" | "outline" | "default" | Plain row, or a bordered/elevated row |
size | "md" | "sm" | "lg" | "md" | Row height / text size |
tooltip | string | TooltipContent props | — | Label shown as a right-side tooltip while collapsed (icon mode) |
render | ReactElement | (props, state) => ReactElement | — | Swap the underlying element (e.g. a router Link) |
SidebarMenuSubButton
Extends React.ComponentProps<'a'> and Base UI useRender.ComponentProps<'a'>.
| Prop | Type | Default | Description |
|---|---|---|---|
isActive | boolean | false | Marks the current sub-route |
size | "sm" | "md" | "md" | Text size |
render | ReactElement | fn | — | Swap the underlying element |
SidebarMenuAction
Extends React.ComponentProps<'button'> and useRender.ComponentProps<'button'>.
| Prop | Type | Default | Description |
|---|---|---|---|
showOnHover | boolean | false | Keep hidden until the menu item is hovered or focused |
SidebarMenuSkeleton
Extends React.ComponentProps<'div'>.
| Prop | Type | Default | Description |
|---|---|---|---|
showIcon | boolean | false | Also render a leading icon skeleton |
useSidebar()
Read or drive sidebar state from anywhere inside the provider. Throws if called outside SidebarProvider.
| Field | Type | Description |
|---|---|---|
state | "expanded" | "collapsed" | Derived from open; mirrors the data-state attribute |
open | boolean | Desktop open state |
setOpen | (open: boolean) => void | Set desktop open state (also writes the cookie) |
openMobile | boolean | Mobile drawer open state |
setOpenMobile | (open: boolean) => void | Set mobile drawer open state |
isMobile | boolean | True below the 768px breakpoint |
toggleSidebar | () => void | Toggle the right surface (desktop vs. mobile drawer) |