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 (defaultOpen, default true). Pass open + onOpenChange to drive it yourself.
  • Toggling writes a sidebar_state cookie (7-day max-age), so the open/collapsed choice survives reloads. SSR can read that cookie and pass defaultOpen to avoid a flash.
  • A global keyboard shortcut — /Ctrl + B — toggles the sidebar.
  • Below 768px the desktop sidebar is replaced by a Sheet drawer; open/setOpen drive desktop, openMobile/setOpenMobile drive 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.

VS
Warehouse
Operations
Dashboard
The Dashboard workspace renders here.

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.

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

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()
SidebarRailbuttonThin click-to-toggle strip on the sidebar’s edge
SidebarHeaderdivTop slot (branding, switchers)
SidebarContentdivScrollable middle slot; holds groups
SidebarFooterdivBottom slot (user menu, support)
SidebarSeparatorSeparatorSidebar-tuned divider
SidebarInputInputSidebar-styled input for an in-rail search box
SidebarGroupdivA labeled section of the content
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, 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
defaultOpenbooleantrueInitial open state (uncontrolled)
openbooleanControlled open state
onOpenChange(open: boolean) => voidFires when open state changes (controlled)
classNamestringMerged onto the wrapper
styleCSSPropertiesMerged 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
variant"default" | "outline""default"Plain row, or a bordered/elevated row
size"md" | "sm" | "lg""md"Row height / text size
tooltipstring | TooltipContent propsLabel shown as a right-side tooltip while collapsed (icon mode)
renderReactElement | (props, state) => ReactElementSwap 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 | fnSwap 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; mirrors the data-state attribute
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 the 768px breakpoint
toggleSidebar() => voidToggle the right surface (desktop vs. mobile drawer)