VESYL UI

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

  • SidebarProvider is uncontrolled by default. Pass open + onOpenChange to drive it yourself.
  • Toggling writes a sidebar_state cookie (7-day max-age). Without defaultOpen, the provider reads that cookie on first render, so the open/collapsed choice survives reloads. SSR can read the same cookie and pass defaultOpen to avoid a flash.
  • Below mobileBreakpoint ("md", 768px, by default; "sm" is 640px), the sidebar becomes a Sheet drawer, full width on phones. Following a link closes it. Open it with SidebarMobileTrigger, a hamburger button that renders only while the drawer is in use, so it can sit in a page header without a breakpoint class.
  • open/setOpen drive the desktop sidebar; openMobile/setOpenMobile drive the drawer. toggleSidebar() picks the right one.
  • The sidebar publishes the width it occupies as --app-sidebar-width on the document root, so portaled surfaces (e.g. Sheet sized to the main column) can stay beside it.

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.

VS
Warehouse
Operations
Dashboard
The Dashboard workspace renders here.

SidebarTrigger is a ghost icon button that calls toggleSidebar(), labelled “Collapse sidebar” / “Expand sidebar” with a matching tooltip. Drop it in your page header (inside SidebarInset), or in the sidebar itself. Inside an expanded sidebar it dims until hovered or focused; inside the mobile drawer it becomes an X that closes the drawer. 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. Each SidebarMenuButton shows its <span> label as a right-side tooltip only while collapsed; pass tooltip to say something else, or tooltip={false} to turn it off. Add a SidebarRail for the thin click-to-toggle strip on the sidebar’s edge.

The rest of the family adapts to the icon column too:

  • SidebarLogo swaps its children for a mark and sizes itself like a menu button, so the mark lines up with the icons.
  • SidebarHeaderRow lays a logo and a SidebarTrigger side by side when expanded, and stacks them (trigger on top) on the icon column.
  • SidebarSeparator shortens to a mark under the icons, and SidebarFooter bordered draws a rule that does the same.
  • SidebarMenuButton size="monogram" holds a short text code (e.g. a location code) in place of an icon. hasActiveChild marks a group trigger whose child page is current.
  • SidebarGroup variant="recessed" sets an expanded cluster into the sidebar surface.

Everything on the icon column stays anchored to the left edge, where the icons sit in both states. Collapsing therefore reads as the column closing over the labels rather than the rows re-centering, and the sidebar clips its contents while the width animates. Pass animate={false} to SidebarProvider to snap the width instead, e.g. when the main pane is too heavy to reflow every frame. Reduced-motion users always get the snap.

<SidebarHeader>
  <SidebarHeaderRow>
    <SidebarLogo render={<Link to="/" />} mark={<LogoMark />}>
      <Logo />
    </SidebarLogo>
    <SidebarTrigger />
  </SidebarHeaderRow>
</SidebarHeader>
Orders
Toggle the trigger to collapse the rail to icons. Hover a collapsed item to see its tooltip label.

In icon mode the SidebarGroupLabel, SidebarMenuSub, SidebarMenuAction, and SidebarMenuBadge parts hide themselves automatically, and SidebarMenuButton collapses to a square. Below mobileBreakpoint the sidebar falls back to the mobile Sheet drawer.

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.

Fulfillment
Workspace
On hold
Nested sub-items live under a Collapsible. The badge and the hover-revealed action share the menu item.

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 the SidebarInset becomes a rounded inset card). web/wms uses "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-styled Input for an in-rail search/filter box; place it in the SidebarHeader.
  • SidebarSeparator — a sidebar-tuned Separator (correct color + inset margins).
  • SidebarGroupAction — an action button pinned to a SidebarGroupLabel (e.g. a ”+” to add to that group).
  • SidebarMenuSkeleton — a loading placeholder row (optional showIcon) 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

PartElementDescription
SidebarProviderdivOwns state + cookie; supplies CSS vars and a TooltipProvider. Required wrapper.
SidebardivThe sidebar container. Props: side, variant, collapsible
SidebarInsetmainThe main content region beside the sidebar; rounds into a card under variant="inset"
SidebarTriggerButtonGhost icon button that calls toggleSidebar()
SidebarMobileTriggerButtonHamburger that opens the drawer; renders nothing when the sidebar is on screen
SidebarRailbuttonThin click-to-toggle strip on the sidebar’s edge
SidebarHeaderdivTop slot (branding, switchers)
SidebarHeaderRowdivLogo + trigger row; stacks on the icon column
SidebarLogodiv*Brand slot. Prop: mark (shown while collapsed to icons)
SidebarContentdivScrollable middle slot; holds groups
SidebarFooterdivBottom slot (user menu, support). Prop: bordered
SidebarSeparatorSeparatorSidebar-tuned divider
SidebarInputInputSidebar-styled input for an in-rail search box
SidebarGroupdivA labeled section of the content. Prop: variant
SidebarGroupLabeldiv*Group heading; hides in icon mode
SidebarGroupActionbutton*Action pinned to a group label
SidebarGroupContentdivWrapper for a group’s body
SidebarMenuulList of menu items
SidebarMenuItemliOne menu row
SidebarMenuButtonbutton*The clickable row. Props: isActive, hasActiveChild, variant, size, tooltip
SidebarMenuActionbutton*Secondary control on a row. Prop: showOnHover
SidebarMenuBadgedivTrailing count/badge on a row
SidebarMenuSkeletondivLoading placeholder row. Prop: showIcon
SidebarMenuSubulNested sub-menu list; hides in icon mode
SidebarMenuSubItemliOne sub-menu row
SidebarMenuSubButtona*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'>.

PropTypeDefaultDescription
defaultOpenbooleancookie, then trueInitial open state (uncontrolled)
openboolean—Controlled open state
onOpenChange(open: boolean) => void—Fires when open state changes (controlled)
mobileBreakpoint"sm" | "md""md"Where the sidebar becomes a drawer (640px or 768px)
animatebooleantruefalse snaps width changes instead of animating them
classNamestring—Merged onto the wrapper
styleCSSProperties—Merged after the --sidebar-width* vars

Extends React.ComponentProps<'div'>.

PropTypeDefaultDescription
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'>.

PropTypeDefaultDescription
isActivebooleanfalseMarks the current route; sets data-active and the active styling
hasActiveChildbooleanfalseA page under this row is current; sets data-active-child
variant"default" | "outline""default"Plain row, or a bordered/elevated row
size"md" | "sm" | "lg" | "monogram""md"Row height / text size; monogram holds a short code in place of an icon
tooltipstring | TooltipContent props | falsethe <span> labelRight-side tooltip while collapsed (icon mode); false turns it off
renderReactElement | (props, state) => ReactElement—Swap the underlying element (e.g. a router Link)

SidebarMenuSubButton

Extends React.ComponentProps<'a'> and Base UI useRender.ComponentProps<'a'>.

PropTypeDefaultDescription
isActivebooleanfalseMarks the current sub-route
size"sm" | "md""md"Text size
renderReactElement | fn—Swap the underlying element

SidebarMenuAction

Extends React.ComponentProps<'button'> and useRender.ComponentProps<'button'>.

PropTypeDefaultDescription
showOnHoverbooleanfalseKeep hidden until the menu item is hovered or focused

SidebarMenuSkeleton

Extends React.ComponentProps<'div'>.

PropTypeDefaultDescription
showIconbooleanfalseAlso render a leading icon skeleton

useSidebar()

Read or drive sidebar state from anywhere inside the provider. Throws if called outside SidebarProvider.

FieldTypeDescription
state"expanded" | "collapsed"Derived from open; the drawer always reads expanded. Mirrors data-state
openbooleanDesktop open state
setOpen(open: boolean) => voidSet desktop open state (also writes the cookie)
openMobilebooleanMobile drawer open state
setOpenMobile(open: boolean) => voidSet mobile drawer open state
isMobilebooleanTrue below mobileBreakpoint
widthstringExpanded width, including a --sidebar-width override
toggleSidebar() => voidToggle the right surface (desktop vs. mobile drawer)