shadcn.io is not affiliated with official shadcn/ui
Controlled State Sidebar Block for React
A layout playground in a fixed-height window with a controlled sidebar: Open, Close, Toggle, a switch and Ctrl or Command with B all drive the provider, and every onOpenChange call is logged with where it came from.
Installation
Overview
Own your sidebar's state with this controlled state sidebar for React with Base UI. A window framed by a hairline with a plus mark on each corner holds the Sidebar whose provider takes open and onOpenChange from the block, so more than its own trigger can move it: Open, Close and Toggle buttons, a Switch labeled "Sidebar open" and Ctrl or Command with B (while focus is inside the window, shown with Kbd keys) all ask the provider, and the rail follows. Beside the controls from the large breakpoint, a log lists the last six onOpenChange calls, newest first, each with its number, its argument and where it came from (the trigger, the rail, a button, the switch or the shortcut), and a readout shows open, state, openMobile and the call count. Below the medium breakpoint the rail is a sheet with its own state, which the page explains. It's built from Sidebar, Switch, Button, Kbd and Guides, with a CSS entrance only. For layouts that sync the rail with a URL, a preference or a store.
FAQ
What makes the sidebar controlled?
Passing open to the provider. It then shows exactly that value, and every attempt to change it, from the trigger, the rail or the shortcut, calls onOpenChange with the value it would like, leaving you to keep it or refuse it. This block keeps every request, which is why the log and the rail always agree; a block that refused one would see the call in the log and the rail stay where it is. Leave open out and the provider keeps the state itself, with defaultOpen for the first value.
How does the log know where a call came from?
It doesn't, from the provider: onOpenChange receives only the new value. The controls on the page and the shortcut tell the block before they ask (a short-lived note in a ref), the trigger notes itself with its onClick, and a call with no note is logged as coming from the rail, the one part that asks on its own. A note older than a second is ignored, so a stale one can't be picked up by a later call. In your app you rarely need the source; the log is here to show how the state moves.
How does Ctrl or Command with B work here?
The provider's own shortcut is off (shortcut={false}), because it listens on the whole page. The block adds a capture listener to the window's own element instead, so the keys work only while focus is inside the window, and they ask the provider like the buttons. The key caps read ⌘ B on Apple devices and Ctrl B everywhere else, swapped in after the page loads. With the provider's shortcut left on, the same keys would also call onOpenChange, from anywhere in the page.
Why do the controls behave differently on a phone?
Below the medium breakpoint the sidebar is a sheet, and the sheet's open state is internal: open doesn't reach it and onOpenChange isn't called for it, so the trigger there isn't logged. The page's controls open and close the sheet directly, label their log lines setOpenMobile, and the readout shows openMobile. If you need to react to the sheet, read openMobile from useSidebar().
What stays in the page, and how do I connect it?
Everything. The open state, the log and the page names live in the block's own state; nothing is saved, and reloading starts over. To sync the rail with something else, replace the block's useState with your source (a URL search parameter, a stored preference, a store) and keep open and onOpenChange as they are. The pages are links to # whose handler shows the page instead of navigating. They're SidebarMenuButtons with render set to an a element.
Is it accessible?
The sidebar is a navigation landmark named after the product, and the open page is marked as the current page. The buttons and the switch are named, the switch reports its state, and the log is a named region whose empty slots are hidden from screen readers. The log doesn't announce each line on its own; one polite status line at the foot says what each call did ("Closed by the switch (call 4)"), so nothing is read twice.
Related
- Pro
Offcanvas Sidebar
VariantsA reading app in a fixed-height window whose sidebar slides fully out of view, so the article's line length grows from 62 to 78 characters; the trigger, the edge rail and Ctrl or Command with B bring it back.
- Pro
Custom Width Sidebar
VariantsA hosting platform's settings console in a fixed-height window where the sidebar's width is the control: four presets set its CSS variable, and a measured count shows how many of ten long labels get cut at each.
- Pro
Right Side Sidebar
VariantsA team file manager in a fixed-height window with its sidebar on the end side: choose a file in the list and the sidebar shows its details, a Copy path button and a Close button, and on a phone it opens as a sheet.
- Pro
Email Client Sidebar
App contentA mail client in a fixed-height window: mailboxes and labels whose counts follow the mail you file, a searchable message list beside the open message, and Compose for a local draft.
- Pro
Sidebar Toggle Navbar
Mobile and drawersA navbar with a sidebar toggle for a project tracker: a button, the [ key and a tooltip fold a mini sidebar under the bar between icons only and icons with labels, and on phones the same button opens it as a sheet.
- Pro
Dark Theme Sidebar
State and themingA developer console in a fixed-height window with three sidebar surfaces made only of theme tokens (flush, tinted and raised), so the rail is right in every preset and in dark mode, beside a terminal that stays dark by nature.