Sidebar

Sidebar

A composable application sidebar with cookie-persisted state and a Cmd/Ctrl+B toggle. Three variants, three collapse modes, and a Sheet on mobile.

Structure

Wrap the page in SidebarProvider, render a Sidebar beside a SidebarInset for the main content, and put a SidebarTrigger anywhere inside the provider. This page documents the API with code only – the sidebar takes over the full viewport, so see the showcase for a live demo.

<SidebarProvider>
  <Sidebar>
    <SidebarHeader />
    <SidebarContent>
      <SidebarGroup>
        <SidebarGroupLabel>Platform</SidebarGroupLabel>
        <SidebarGroupContent>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton>
                <Home />
                <span>Dashboard</span>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarGroupContent>
      </SidebarGroup>
    </SidebarContent>
    <SidebarFooter />
    <SidebarRail />
  </Sidebar>
  <SidebarInset>
    <header className="flex h-14 items-center border-b px-4">
      <SidebarTrigger />
    </header>
    <div className="p-6">Page content</div>
  </SidebarInset>
</SidebarProvider>

Installation

npx shadcn@latest add https://espresso-base-ui.vercel.app/r/sidebar.json

Usage

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar"

The provider usually lives in a layout so the sidebar persists across routes. It reads and writes a sidebar_state cookie (7-day max-age), so you can restore the last state on the server:

// app/layout.tsx
import { cookies } from "next/headers"

export default async function Layout({ children }) {
  const cookieStore = await cookies()
  const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"

  return (
    <SidebarProvider defaultOpen={defaultOpen}>
      <AppSidebar />
      <SidebarInset>{children}</SidebarInset>
    </SidebarProvider>
  )
}

Collapsible modes

collapsible controls what happens when the sidebar closes: offcanvas (default) slides it fully out of view, icon shrinks it to a 3rem icon rail – menu buttons show their tooltip and labels, badges and submenus hide – and none renders a fixed, non-collapsing column.

<Sidebar collapsible="offcanvas" />  // slides out of view (default)
<Sidebar collapsible="icon" />       // collapses to a 3rem icon rail
<Sidebar collapsible="none" />       // always expanded

side and variant combine with any mode: floating wraps the content in a padded, rounded, ring-bordered card and inset pairs with SidebarInset to render the main content as a raised, rounded panel.

<Sidebar side="right" variant="floating" collapsible="icon">
  ...
</Sidebar>

useSidebar

Any component under the provider can read and drive the sidebar through the useSidebar hook. It throws if called outside a SidebarProvider.

function CloseOnNavigate() {
  const { isMobile, setOpenMobile, toggleSidebar } = useSidebar()

  return (
    <SidebarMenuButton
      onClick={() => {
        if (isMobile) setOpenMobile(false)
      }}
    >
      ...
    </SidebarMenuButton>
  )
}

useSidebar()

PropTypeDefaultDescription
state"expanded" | "collapsed""expanded"Derived desktop state; mirrors open and is reflected on the sidebar as data-state.
openbooleantrueDesktop open state.
setOpen(open: boolean) => void–Sets the desktop state and persists it in the sidebar_state cookie.
openMobilebooleanfalseWhether the mobile Sheet is open.
setOpenMobile(open: boolean) => void–Opens or closes the mobile Sheet.
isMobilebooleanfalseTrue below the 768px breakpoint, via the useIsMobile hook (false during the first render, before the media query resolves).
toggleSidebar() => void–Toggles the sidebar for the current device – desktop open state or the mobile Sheet. Also bound to Cmd/Ctrl+B.

API reference

Two components take configuration props; every other part is a styled wrapper that accepts className and the props of the element it renders.

SidebarProvider

PropTypeDefaultDescription
defaultOpenbooleantrueInitial desktop open state when uncontrolled.
openboolean–Controlled open state; pair with onOpenChange. Cookie persistence still runs on change.
onOpenChange(open: boolean) => void–Called with the next state on every toggle.
styleCSSProperties–Merged over the wrapper's CSS variables – override --sidebar-width (16rem) and --sidebar-width-icon (3rem) here to resize the sidebar.

Sidebar

PropTypeDefaultDescription
side"left" | "right""left"Which edge of the viewport the sidebar docks to.
variant"sidebar" | "floating" | "inset""sidebar"Visual treatment: flush full-height column with a border, a floating rounded card, or the inset layout where SidebarInset renders the page as a raised panel.
collapsible"offcanvas" | "icon" | "none""offcanvas"Collapse behavior when closed: slide fully away, shrink to an icon rail, or never collapse.

SidebarMenuButton

PropTypeDefaultDescription
isActivebooleanfalseMarks the current item – raised surface background, foreground text and a shadow, reflected as data-active.
variant"default" | "outline""default"Default is transparent with an accent hover; outline adds a background and a hairline shadow ring.
size"default" | "sm" | "lg""default"Row height and text size: h-8, h-7 with text-xs, or h-10 for header-style rows with avatars.
tooltipstring | TooltipContent props–Tooltip shown to the right of the button, only while the sidebar is icon-collapsed on desktop.
renderReactElement | function–Base UI render prop to replace the default button element, e.g. a Link.

SidebarMenuSubButton

PropTypeDefaultDescription
isActivebooleanfalseAccent background and text for the current sub-item, reflected as data-active.
size"sm" | "md""md"Text size of the sub-link: text-xs or text-sm (both rows are h-7).
renderReactElement | function–Base UI render prop to replace the default anchor element.

SidebarMenuAction

PropTypeDefaultDescription
showOnHoverbooleanfalseOn desktop, keeps the action invisible until the row is hovered or focused (always visible on touch).

SidebarMenuSkeleton

PropTypeDefaultDescription
showIconbooleanfalseAdds a square icon placeholder before the text bar.

Parts

Every part tags itself with a data-slot attribute (and most with a legacy data-sidebar attribute) for CSS targeting:

ComponentDescription
SidebarProviderContext provider and flex wrapper (data-slot="sidebar-wrapper"). Defines --sidebar-width and --sidebar-width-icon and binds the Cmd/Ctrl+B shortcut.
SidebarThe sidebar itself (data-slot="sidebar"). On desktop renders a gap element plus a fixed container (data-slot="sidebar-gap", "sidebar-container", "sidebar-inner"); on mobile renders a Sheet.
SidebarTriggerGhost icon Button with a PanelLeft icon and sr-only label that calls toggleSidebar (data-slot="sidebar-trigger").
SidebarRailInvisible grab strip along the sidebar edge that toggles on click, with directional resize cursors (data-slot="sidebar-rail").
SidebarInsetThe main element for page content (data-slot="sidebar-inset"). With variant="inset" it becomes a rounded, shadowed panel.
SidebarHeader / SidebarFooterSticky top and bottom zones of the column (data-slot="sidebar-header" / "sidebar-footer").
SidebarContentScrollable middle region with a scroll-driven shadow mask (data-slot="sidebar-content").
SidebarInputInput restyled for the sidebar surface (data-slot="sidebar-input").
SidebarSeparatorSeparator on the sidebar border color with horizontal margins (data-slot="sidebar-separator").
SidebarGroupPadded section within the content area (data-slot="sidebar-group").
SidebarGroupLabelSmall heading row; fades and slides away in icon mode (data-slot="sidebar-group-label"). Accepts render.
SidebarGroupActionIcon button in the group's top-right corner; hidden in icon mode (data-slot="sidebar-group-action"). Accepts render.
SidebarGroupContentPlain wrapper for the group body (data-slot="sidebar-group-content").
SidebarMenu / SidebarMenuItemul and li primitives for a menu list (data-slot="sidebar-menu" / "sidebar-menu-item").
SidebarMenuButtonThe main row control (data-slot="sidebar-menu-button"). Reflects data-size and data-active; shrinks to a square icon button in icon mode.
SidebarMenuActionTrailing icon button positioned over the row (data-slot="sidebar-menu-action"), aligned per button size.
SidebarMenuBadgeNon-interactive count pinned to the row edge; hidden in icon mode (data-slot="sidebar-menu-badge").
SidebarMenuSkeletonLoading placeholder row with optional icon and a randomized text width (data-slot="sidebar-menu-skeleton").
SidebarMenuSub / SidebarMenuSubItemNested list for child links; hidden in icon mode (data-slot="sidebar-menu-sub" / "sidebar-menu-sub-item").
SidebarMenuSubButtonIndented anchor row for a child link (data-slot="sidebar-menu-sub-button"). Reflects data-size and data-active.

Behavior & styling hooks

The provider listens for Cmd/Ctrl+B to toggle the sidebar and writes every change to a sidebar_state cookie with a 7-day max-age. Below 768px (useIsMobile) the sidebar renders inside a Sheet with an sr-only title and description, controlled by openMobile instead of open. On desktop the outer element exposes data-state="expanded | collapsed", data-collapsible (set only while collapsed), data-variant and data-side – internal parts style themselves against these via group-data-[collapsible=icon]-style selectors, and you can target them the same way. Widths come from the --sidebar-width and --sidebar-width-icon CSS variables, and colors from the bg-sidebar theme token family.

Search documentation

Search the component docs