Get started
Fabricator UI

Search documentation

Search pages, components, blocks, styles and colours.

Sidebar

PreviousNext

An app sidebar that drags to resize, peeks out when collapsed and becomes a drawer on mobile.

The sidebar is the frame of an app: a header, scrolling sections of rows, a footer, and the main area beside it. Drag its edge to resize it, click the edge or press ⌘B to collapse it, and hover the collapsed edge to peek at it. Below 768px it becomes a drawer.

Rows share one hover highlight that glides from row to row, and the current row's background slides when the selection moves. Sections collapse like an accordion, rows take status dots, badges and actions, and the toggle's panel glyph animates between its two states.

Installation

bunx --bun fabricator-ui@latest add sidebar

Usage

app/layout.tsx
import { cookies } from "next/headers"
 
import {
  SidebarInset,
  SidebarProvider,
  SidebarTrigger,
} from "@/components/ui/sidebar"
import { AppSidebar } from "@/components/app-sidebar"
 
export default async function Layout({
  children,
}: {
  children: React.ReactNode
}) {
  const cookieStore = await cookies()
  const defaultOpen = cookieStore.get("sidebar_state")?.value !== "false"
 
  return (
    <SidebarProvider defaultOpen={defaultOpen}>
      <AppSidebar />
      <SidebarInset>
        <header className="flex h-12 items-center px-3">
          <SidebarTrigger />
        </header>
        {children}
      </SidebarInset>
    </SidebarProvider>
  )
}
components/app-sidebar.tsx
"use client"
 
import { InboxIcon, SearchIcon } from "lucide-react"
 
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarRail,
} from "@/components/ui/sidebar"
 
export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarHeader />
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel>Workspace</SidebarGroupLabel>
          <SidebarGroupContent>
            <SidebarMenu>
              <SidebarMenuItem>
                <SidebarMenuButton icon={InboxIcon} isActive>
                  <span>Inbox</span>
                </SidebarMenuButton>
              </SidebarMenuItem>
              <SidebarMenuItem>
                <SidebarMenuButton icon={SearchIcon}>
                  <span>Search</span>
                </SidebarMenuButton>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarGroupContent>
        </SidebarGroup>
      </SidebarContent>
      <SidebarFooter />
      <SidebarRail />
    </Sidebar>
  )
}

The provider saves the open state to the sidebar_state cookie, so a server layout can read it back into defaultOpen and the sidebar renders in the right state on the first paint.

Composition

SidebarProvider
├── Sidebar
│   ├── SidebarHeader
│   ├── SidebarContent
│   │   └── SidebarGroup
│   │       ├── SidebarGroupLabel
│   │       ├── SidebarGroupAction (or SidebarGroupActions)
│   │       └── SidebarGroupContent
│   │           └── SidebarMenu
│   │               └── SidebarMenuItem
│   │                   ├── SidebarMenuButton
│   │                   ├── SidebarMenuAction (or SidebarMenuActions)
│   │                   ├── SidebarMenuBadge
│   │                   └── SidebarMenuSub
│   │                       └── SidebarMenuSubItem
│   │                           └── SidebarMenuSubButton
│   ├── SidebarFooter
│   └── SidebarRail
└── SidebarInset
    └── SidebarTrigger
  • SidebarProvider holds the state: open or collapsed, the mobile drawer, the live width, the shortcut and peek.
  • Sidebar is the panel. SidebarInset is the main area next to it.
  • SidebarHeader and SidebarFooter stay put; SidebarContent scrolls between them, fading its rows out at the edges.
  • SidebarGroup is a section: a label, optional actions, and its rows.
  • SidebarMenu is a list of rows. Each SidebarMenuItem holds a SidebarMenuButton and, optionally, actions, a badge and a sub-menu.
  • SidebarRail is the handle on the sidebar's edge. SidebarTrigger toggles the sidebar from anywhere inside the provider.

Examples

Floating

variant="floating" lifts the panel into a rounded card. The main demo above uses variant="inset", which turns the main area into the card instead.

Collapsible sections

collapsible on SidebarGroup turns its label into a toggle for everything after it. A chevron appears on hover and stays visible while the section is closed. SidebarGroupActions holds up to three actions on the label row.

Status dots

status draws a dot in the icon column: unread is filled, idle is a ring, and active is filled and marks the row as the current one. Screen readers hear "Unread" before an unread row. For a dot without that meaning, use dot="filled" or dot="ring".

Actions and badges

A badge sits at the end of the row and turns semibold on the active row. An action sits beside it. showOnHover hides actions until the row is hovered or focused, and SidebarMenuActions groups several. The label always stops short of them.

Nested rows

open on SidebarMenuSub collapses the sub-tree on the moderate spring; wire it to the parent row. The hover highlight glides from the parent into its children, and arrow keys skip a closed sub-tree.

Peek

With peek="hover" on the provider, a collapsed sidebar floats out as a card when the pointer rests on its edge or on the trigger, and tucks away when the pointer leaves. peek="click" opens it on a click of the edge instead. Escape or a press outside dismisses it; clicking a trigger pins it open.

Put a SidebarTrigger in the sidebar's header as well: the card covers the main area's trigger while it is out.

Right side

side="right" puts the sidebar on the right edge, and the toggle glyph mirrors. Here the shortcut is a bare ].

Collapse to icons

collapsible="icon" keeps a 48px column of icons when collapsed. Give each row a tooltip so its name shows on hover.

Controlled

const [open, setOpen] = React.useState(false)
 
return (
  <SidebarProvider open={open} onOpenChange={setOpen}>
    <Sidebar />
  </SidebarProvider>
)

Resize and collapse

SidebarRail is the sidebar's edge handle. Drag it to resize between 160px and 360px; drag past the minimum and the sidebar collapses (pull back before letting go to keep it open). Click it to collapse. Its tooltip explains both and shows the shortcut. Leave it out for a fixed width.

The starting width is width on the provider (16rem by default). useSidebar() returns the live width and setWidth, so you can save it:

const { width } = useSidebar()
 
React.useEffect(() => {
  localStorage.setItem("sidebar-width", width)
}, [width])

Keyboard shortcut

⌘B (Ctrl B on Windows and Linux) toggles the sidebar. Set shortcut on the provider to change it: "mod+b" for the default, or a bare key such as "[", which is ignored while typing in a field. shortcut={null} turns it off. With providers nested, the innermost one around the focus answers.

Motion

  • Opening and closing move on the moderate tier: the spring to open (210ms), the exit tween to close (160ms). A peeking card slides in on the same spring.
  • While the rail is dragged, the width follows the pointer with no transition.
  • Sections and sub-trees collapse on the moderate tier, height and opacity together.
  • The hover highlight glides on the fast tier; the active row's background slides on the moderate one.
  • The toggle's panel glyph morphs on the moderate spring (browsers that can't animate an SVG path swap it).
  • With reduced motion, the sidebar, the card and the glyph change at once, and collapses only fade.

API Reference

SidebarProvider

PropTypeDefault
defaultOpenbooleantrue
openboolean-
onOpenChange(open: boolean) => void-
persistbooleantrue
shortcutstring | null"mod+b"
peek"none" | "hover" | "click""none"
widthstring"16rem"
widthMobilestring"18rem"

persist saves the desktop state to the sidebar_state cookie for 7 days; the drawer's state isn't saved. Peek works with collapsible="offcanvas". The width is also published as --sidebar-width; a --sidebar-width set in style is used as the starting width.

PropTypeDefault
side"left" | "right""left"
variant"sidebar" | "floating" | "inset""sidebar"
collapsible"offcanvas" | "icon" | "none""offcanvas"
borderedbooleantrue

bordered draws the sidebar variant's inner-edge border. With the inset variant, wrap the main content in SidebarInset.

useSidebar

PropertyType
state"expanded" | "collapsed"
open, setOpenboolean, (open: boolean) => void
openMobile, setOpenMobileboolean, (open: boolean) => void
isMobileboolean
toggleSidebar() => void
width, setWidthstring, (width: string) => void
side"left" | "right"
shortcutstring | null
peek, isPeeking, setIsPeeking"none" | "hover" | "click", boolean, setter
isResizingboolean

SidebarTrigger

A ghost icon button that calls toggleSidebar(). Its tooltip names the action with the shortcut. Pass children to replace the panel glyph. With peek="hover", resting on it while collapsed peeks the sidebar.

SidebarRail

Drag to resize, click to collapse (or expand). It takes <button> props and isn't in the tab order: keyboard users have the trigger and the shortcut.

SidebarGroup

PropTypeDefault
collapsiblebooleanfalse
openboolean-
defaultOpenbooleantrue
onOpenChange(open: boolean) => void-

Collapsible groups need a SidebarGroupLabel child; everything after it (other than the header actions) collapses. You can still wrap a group in a Collapsible instead.

SidebarMenuButton

PropTypeDefault
isActivebooleanfalse
iconReact.ComponentType-
status"active" | "unread" | "idle"-
dot"filled" | "ring"-
variant"default" | "outline""default"
size"default" | "sm" | "lg""default"
tooltipstring | TooltipContent props-

To render a link, use asChild with a single child element (<Link href="/inbox" />):

<SidebarMenuButton asChild icon={InboxIcon} isActive>
  <a href="/inbox">Inbox</a>
</SidebarMenuButton>

icon takes the component itself, so the file that passes it must be a Client Component. In a Server Component, put the icon in the children instead (<InboxIcon /> before the label). tooltip shows only while collapsed to icons.

SidebarMenuAction, SidebarMenuActions

PropTypeDefault
showOnHoverbooleanfalse

SidebarMenuActions wraps several SidebarMenuActions; set showOnHover on the wrapper. SidebarGroupActions does the same for SidebarGroupActions on a section's label row. Give every action an aria-label.

SidebarMenuSub, SidebarMenuSubButton

ComponentPropTypeDefault
SidebarMenuSubopenboolean-
SidebarMenuSubButtonisActivebooleanfalse
SidebarMenuSubButtonsize"sm" | "md""md"
SidebarMenuSubButtoniconReact.ComponentType-

Without open the sub-menu is always shown.

SidebarMenuBadge, SidebarMenuSkeleton

SidebarMenuBadge takes <div> props. SidebarMenuSkeleton takes showIcon and renders a loading row.

Data attributes

AttributeElementPresent when
data-state="expanded|collapsed"[data-slot=sidebar]always (desktop)
data-collapsible[data-slot=sidebar]collapsed: offcanvas or icon
data-peek[data-slot=sidebar]peek is on and the sidebar collapsed
data-peeking[data-slot=sidebar]the card is out
data-resizing[data-slot=sidebar]the rail is being dragged
data-activemenu and sub buttonsthe current row
data-statusSidebarMenuButtonstatus is set
data-state="open|closed"SidebarGroup, SidebarMenuSubcollapsible / open is set

Theming

The panel uses the sidebar color tokens (--sidebar, --sidebar-foreground, --sidebar-border, --sidebar-ring, …). In the Fabricator style the panel is surface 2, rows are muted at rest and foreground when lit, the hover highlight is bg-hover and the active row bg-active.

Accessibility

  • The trigger is a button named "Toggle Sidebar"; its tooltip says whether it will expand or collapse and shows the shortcut.
  • The active row has aria-current="page". Arrow Up and Down move between rows (sub-rows included, closed ones skipped), Home and End jump to the first and last, and Tab still visits each row. A row that opens a menu keeps the menu's own arrow keys.
  • A collapsible section's label is a button with aria-expanded and aria-controls. A closed section or sub-tree is inert, so its rows leave the tab order.
  • Status dots are visual; status="unread" adds "Unread" for screen readers.
  • The rail is pointer-only (tabIndex=-1); the peek edge is a button named "Show sidebar" with aria-expanded.
  • On mobile the sidebar is a modal drawer: focus is trapped inside, Escape and a press on the scrim close it.
  • Every interactive part shows the 1px focus ring. The rail has a resize cursor; rows, actions and labels a pointer.
  • With reduced motion the sidebar, peek and glyph change without travel.