Skip to content

ComponentsNavigation

Sidebar

A composable application sidebar. Collapses off-canvas or to an icon rail, comes in sidebar, floating and inset styles, turns into a modal sheet on small screens, and toggles with ⌘B / Ctrl+B. Menu items can be links or buttons, with badges, groups and tooltips in icon mode.

Source
Home

Installation

pnpm dlx shadcn@latest add @desyne/sidebar

The CLI installs dependencies and any other components this one uses.

Usage

tsx
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarSeparator,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar";
app/(app)/layout.tsxtsx
export default function AppLayout({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <Sidebar collapsible="icon">
        <SidebarHeader>{/* workspace switcher */}</SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Application</SidebarGroupLabel>
            <SidebarMenu>
              <SidebarMenuItem>
                <SidebarMenuButton href="/inbox" tooltip="Inbox">
                  <InboxIcon />
                  <span>Inbox</span>
                </SidebarMenuButton>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarGroup>
        </SidebarContent>
        <SidebarFooter>{/* user menu */}</SidebarFooter>
      </Sidebar>
      <SidebarInset>
        <header>
          <SidebarTrigger />
        </header>
        {children}
      </SidebarInset>
    </SidebarProvider>
  );
}

SidebarProvider owns the open state and renders the layout wrapper (min-h-svh, full width, flex row). Sidebar reserves its width in that row and positions the panel fixed to the viewport, so the page content in SidebarInset never sits underneath it.

Rendering inside a box

The sidebar is fixed to the viewport by default. To keep it inside a bounded element (a preview, a modal, a dashboard widget), give an ancestor data-sidebar-contained and relative. The panel then uses absolute positioning and the ancestor's height. Every preview on this page does this, and sets className="h-full min-h-0" on SidebarProvider.

When to use

  • Sidebar — persistent primary navigation for an application with several top-level sections, especially when users switch between them often.
  • Tabs — a handful of sibling views within one page or object.
  • Sheet — temporary, secondary content that slides in over the page. On small screens the sidebar already uses a sheet for you.
  • Menu — a list of actions, or navigation that should stay hidden until requested. Use it inside the sidebar for workspace and account switchers.

Anatomy

tsx
<SidebarProvider>                  {/* state, shortcut, CSS variables, layout row */}
  <Sidebar>                        {/* panel: desktop column or mobile sheet */}
    <SidebarHeader />              {/* top: switcher, search */}
    <SidebarContent>               {/* scrolls */}
      <SidebarGroup>
        <SidebarGroupLabel />
        <SidebarMenu>
          <SidebarMenuItem>
            <SidebarMenuButton />  {/* link (href) or button */}
            <SidebarMenuBadge />   {/* optional count */}
          </SidebarMenuItem>
        </SidebarMenu>
      </SidebarGroup>
    </SidebarContent>
    <SidebarSeparator />
    <SidebarFooter />              {/* bottom: user menu, help */}
  </Sidebar>
  <SidebarInset>                   {/* main content */}
    <SidebarTrigger />
  </SidebarInset>
</SidebarProvider>
PartRendersNotes
SidebarProvider<div>Provides context, the ⌘B / Ctrl+B shortcut and the --sidebar-width variables. Wraps both the sidebar and the inset.
Sidebar<div> (desktop), Modal › Dialog (mobile), <div> (collapsible="none")The panel. On desktop it renders a spacer (sidebar-gap) plus a fixed container (sidebar-container › sidebar-inner).
SidebarTriggerButtonGhost icon button that calls toggleSidebar(). Renders its children in place of the default icon.
SidebarInset<main>The page content next to the sidebar. Gets a margin, border and rounded corners when the sidebar uses variant="inset".
SidebarHeader / SidebarFooter<div>Fixed regions above and below the content.
SidebarContent<div>Takes the remaining height and scrolls. Overflow is hidden in icon mode.
SidebarGroup<div>A section of the sidebar.
SidebarGroupLabel<div>Small uppercase heading. Fades out and collapses in icon mode.
SidebarMenu<ul>A list of menu items.
SidebarMenuItem<li>Positions a button and its badge.
SidebarMenuButtonLink (<a>) or ButtonA link when href is set (with aria-current="page" when active), a button otherwise. Optional tooltip in icon mode.
SidebarMenuBadge<div>Count or status at the end of a menu item. Hidden in icon mode.
SidebarSeparatorSeparatorHorizontal rule with the sidebar border color.

Examples

Variants

variant changes how the panel sits in the layout. sidebar (default) is a full-height column with a border. floating insets the panel with a margin, rounded corners and a shadow.

inset puts the panel on the sidebar background and turns SidebarInset into a raised, rounded card. Use it for dashboards where the content should read as one surface.

It works on either side. With side="right", render SidebarInset before the Sidebar (as for any right sidebar); the card drops its margin on the side next to the panel.

Collapsible modes

collapsible controls what happens when the sidebar is closed on desktop: offcanvas (default) slides it out of view, icon shrinks it to a 3rem rail of icons, and none renders a static panel that can't collapse. Switch modes in the preview and press the trigger.

In icon mode, group labels and badges hide, menu buttons become 32px squares, and text in the last <span> of each button is clipped. Set tooltip on every SidebarMenuButton so collapsed items keep a visible label on hover and focus.

Right side

side="right" docks the panel on the right, for details and inspector panels. Render SidebarInset before the Sidebar so the space is reserved on the correct side. On mobile the sheet slides in from the right. Pass an icon as the trigger's children to point it the right way.

ENG-482 · Checkout retries

Payments occasionally fail with a timeout from the card network. Retry idempotent requests up to three times with exponential backoff.

Split long navigation into SidebarGroups with a SidebarGroupLabel each. SidebarFooter stays pinned to the bottom while SidebarContent scrolls; a SidebarSeparator divides them.

size="lg" (48px) for workspace and user switchers with two lines of text, default (32px) for primary navigation, and sm (28px) for dense or secondary lists.

lg
default
sm

Collapsible submenus

Nest a SidebarMenu inside a React Aria Disclosure, and give the SidebarMenuButton slot="trigger" to toggle it. The button gets aria-expanded automatically. Animate the panel height with --disclosure-panel-height, as the Disclosure component does.

Active item and routing

Mark the current page with isActive; for links it also sets aria-current="page". Derive it from your router's current path, and wrap the app in React Aria's RouterProvider so menu links use client-side navigation. The preview uses a local stand-in for the router.

Overview
Rendered for #overview.

With the Next.js App Router:

app/providers.tsxtsx
"use client";

import { useRouter } from "next/navigation";
import { RouterProvider } from "react-aria-components";

export function Providers({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  return <RouterProvider navigate={router.push}>{children}</RouterProvider>;
}
components/nav-main.tsxtsx
"use client";

import { usePathname } from "next/navigation";

export function NavMain({ items }: { items: { href: string; title: string; icon: React.ElementType }[] }) {
  const pathname = usePathname();
  return (
    <SidebarMenu>
      {items.map((item) => (
        <SidebarMenuItem key={item.href}>
          <SidebarMenuButton href={item.href} isActive={pathname === item.href} tooltip={item.title}>
            <item.icon />
            <span>{item.title}</span>
          </SidebarMenuButton>
        </SidebarMenuItem>
      ))}
    </SidebarMenu>
  );
}

Controlled

Pass open and onOpenChange to SidebarProvider to own the desktop state, for example to persist it in a cookie or user setting. On desktop, the trigger, the keyboard shortcut and anything calling setOpen all go through onOpenChange. The mobile sheet keeps its own state.

The sidebar is expanded.
tsx
// Persist the state between visits.
const [open, setOpen] = useState(defaultOpenFromCookie);

<SidebarProvider
  open={open}
  onOpenChange={(value) => {
    setOpen(value);
    document.cookie = `sidebar_state=${value}; path=/; max-age=31536000`;
  }}
/>;

useSidebar

Any component inside SidebarProvider can read and change the state with useSidebar(). Use it for custom triggers, or to adapt content to the collapsed state. It throws if called outside a provider.

Static sidebar

collapsible="none" renders a plain, always-visible <div> with no container, spacer or mobile sheet. Use it for secondary navigation inside a page, such as settings.

It stays inline on every screen size, as in shadcn/ui: below 768px it doesn't become a sheet or hide, and SidebarTrigger and the shortcut don't affect it. It's a normal flex item, so it never overlays the page, but in a row it shrinks with the layout; on narrow screens you may want to stack it above the content or swap it for a Select or Tabs.

Appearance

Choose a theme and density for your workspace.

Mobile

Below 768px (max-width: 767px) the desktop panel is hidden, and Sidebar renders as a modal sheet from its side instead. The trigger and the shortcut toggle the sheet (openMobile), which is separate from the desktop open state, so collapsing on desktop doesn't affect mobile. The sheet has a backdrop, traps focus, and closes on Esc or an outside click.

Pressing a link inside the sheet closes it, with the mouse, touch or Enter. This covers any <a href> in the sheet, whether it's a SidebarMenuButton link or a link in your own markup. Clicks that open a new tab or window (with a modifier key, target="_blank" or download) leave it open, and so do button items. Pass closeOnNavigate={false} to keep the sheet open, or call setOpenMobile(false) from useSidebar() to close it after a button press.

className and style on Sidebar also apply to the sheet. Use max-md: classes to style only the sheet, and md: classes to style only the desktop panel.

Home

On a phone, open the sheet and pick a page: it closes as the route changes.

The breakpoint is based on the viewport width (via useIsMobile()), not the container, so contained previews on a wide screen always show the desktop sidebar. Resize the browser window to see the sheet.

Recipes

App shell

An inset sidebar with icon collapsing: a workspace switcher Menu in the header, primary navigation with a badge, a projects group, and an account menu in the footer. The content area has a trigger and Breadcrumbs.

  1. Projects
  2. Checkout v2

Mail

Folders and labels with unread counts in SidebarMenuBadge. The Compose button reads useSidebar() to collapse to an icon and show its tooltip only in icon mode, matching the menu buttons.

Inbox
  • Stripe9:41
    Your payout of $4,120.00 is on the way
  • Lena Park8:15
    Re: Offer letter for the design role
  • LinearYesterday
    3 issues were assigned to you

Documentation

A documentation layout: title and SearchField in the header, small link items grouped by section, and a scrolling content area.

Components

Sidebar

A collapsible app sidebar. The navigation scrolls on its own while the header with search stays in place.

Accessibility

  • SidebarMenuButton renders a real link when given href, with aria-current="page" when isActive, so middle-click, "open in new tab" and screen-reader link lists work. Without href it's a button.
  • Wrap navigation in landmarks as your layout needs; SidebarInset renders the page's <main>.
  • In icon mode, give every SidebarMenuButton a tooltip. The text stays in the DOM and keeps naming the item for assistive tech; the tooltip gives sighted users the same label.
  • Tooltips only show when the sidebar is collapsed to icons on desktop; they're disabled when expanded and on mobile.
  • SidebarTrigger has aria-label="Toggle sidebar". Pass your own aria-label to change it.
  • On mobile the sidebar is a modal Dialog labelled "Sidebar": focus moves into it, is trapped while open and returns to the trigger on close.
  • The ⌘B / Ctrl+B shortcut is registered on window by each SidebarProvider and calls preventDefault(), so it replaces any browser or editor shortcut on the same keys while the page is focused. Advertise it with a Kbd hint.

Keyboard

KeyAction
⌘+B / Ctrl+BToggles the sidebar (the sheet on mobile)
Tab / Shift+TabMoves between menu items and other controls
EnterFollows a link item, or presses a button item
SpacePresses a button item (or a submenu trigger)
EscCloses the mobile sheet

Styling

Data attributes

On the desktop Sidebar root (data-slot="sidebar", which also has the group and peer classes):

AttributeValues
data-stateexpanded or collapsed
data-collapsibleThe collapsible mode while collapsed (offcanvas or icon); empty while expanded
data-variantsidebar, floating or inset
data-sideleft or right

Style anything inside the sidebar by state with group-data-*, for example group-data-[collapsible=icon]:hidden to hide an element in icon mode. Style siblings after it with peer-data-*. SidebarInset styles itself for the inset variant from the wrapper (group-has-* on sidebar-wrapper), so it works whether it comes before or after the Sidebar. The mobile sheet instead has data-slot="sidebar", data-mobile="true" and data-side.

On SidebarMenuButton:

AttributePresent when
data-active"true" when isActive, "false" otherwise
data-hovered / data-pressedHovered / being pressed
data-focused / data-focus-visibleFocused / focused with the keyboard
data-disabledisDisabled is true
data-currentLink items with isActive (from aria-current)

Slots

data-slotElement
sidebar-wrapperSidebarProvider root
sidebarSidebar root
sidebar-gapSpacer that reserves the sidebar's width on desktop
sidebar-containerThe fixed (or contained absolute) panel on desktop
sidebar-innerThe panel's background surface
sidebar-triggerSidebarTrigger
sidebar-insetSidebarInset
sidebar-header, sidebar-content, sidebar-footerRegions
sidebar-group, sidebar-group-labelGroups
sidebar-menu, sidebar-menu-item, sidebar-menu-button, sidebar-menu-badgeMenu parts
sidebar-separatorSidebarSeparator

CSS variables

VariableDefaultUsed for
--sidebar-width16remExpanded width, desktop and mobile
--sidebar-width-icon3remWidth in icon mode

Set them on SidebarProvider with style:

tsx
<SidebarProvider
  style={{ "--sidebar-width": "18rem", "--sidebar-width-icon": "3.5rem" } as React.CSSProperties}
/>

The colors come from theme tokens: --sidebar (background), --sidebar-foreground, --sidebar-border, --sidebar-accent and --sidebar-accent-foreground (active item), --sidebar-primary (active item icon) and --sidebar-ring. See Theming.

Customizing

  • On desktop, className and other props on Sidebar go to sidebar-container, the fixed panel. With collapsible="none" they go to the root <div>. On mobile, className and style go to the sheet (the Modal); other <div> props aren't applied there.
  • SidebarMenuButton truncates its last <span>, so wrap labels in a <span> and put icons first.
  • SidebarMenuBadge is absolutely positioned for the default button height. Add padding to the button (pr-8) if long labels could run under it.

sidebarMenuButtonVariants

The tailwind-variants function behind SidebarMenuButton. Use it to give other elements, such as a Next.js Link, the look of a menu item.

tsx
import { sidebarMenuButtonVariants } from "@/components/ui/sidebar";

sidebarMenuButtonVariants({ size: "sm" });

API Reference

SidebarProvider

Prop

Type

Also accepts every prop of <div>.

Prop

Type

Also accepts every prop of <div>.

SidebarMenuButton

Prop

Type

Also accepts every prop of React Aria's Link (with href) or Button (without).

SidebarTrigger

A ghost, icon-sm Button with a panel icon. Pass children to replace the icon, for example <PanelRightIcon /> for a right sidebar, or text with size="sm". It keeps aria-label="Toggle sidebar" unless you pass your own, so with visible text pass a matching aria-label (or aria-label={undefined}). Accepts every button prop; onPress runs before the sidebar toggles.

SidebarInset

Accepts every prop of <main>.

SidebarHeader / SidebarFooter / SidebarContent / SidebarGroup / SidebarGroupLabel / SidebarMenuBadge

Accept every prop of <div>.

SidebarMenu / SidebarMenuItem

Accept every prop of <ul> / <li>.

SidebarSeparator

Accepts every prop of Separator.

useSidebar

Returns the sidebar state. Must be called inside SidebarProvider.

Prop

Type

useIsMobile

useIsMobile(): boolean — true when (max-width: 767px) matches. Subscribes to the media query, and returns false during server rendering.

  • Sheet — the modal panel the sidebar becomes on mobile.
  • Menu — workspace and account switchers.
  • Tooltip — labels for icon-only items.
  • Breadcrumbs — show the current location in the content header.
  • Tabs — navigation between views within a page.