Sidebar
App navigation that collapses to a rail you can peek open, resizes by drag or keys, and restores its saved state before the first paint, with no server code.
Installation
npx shadcn@latest add @quireui/sidebarUsage
SidebarProvider holds the state and lays out the two columns. Put the Sidebar and the page's SidebarInset inside it.
<SidebarProvider storageKey="sidebar">
<Sidebar>
<SidebarHeader>{/* logo or workspace switcher */}</SidebarHeader>
<SidebarContent>
<SidebarGroup label="Product">
<SidebarItem icon={<ChartLineIcon />} render={<Link href="/" />} active>
Overview
</SidebarItem>
<SidebarItem icon={<FlagIcon />} render={<Link href="/goals" />} badge={3}>
Goals
</SidebarItem>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>{/* the signed-in user */}</SidebarFooter>
</Sidebar>
<SidebarInset>
<header>
<SidebarTrigger />
</header>
{children}
</SidebarInset>
</SidebarProvider>The page scrolls with the window and the sidebar stays in place. To fit it inside something smaller, give the provider a height and SidebarInset an overflow-auto.
Collapsing
SidebarTrigger or ⌘B collapses it. collapsible says to what:
iconleaves a rail of icons. They stay exactly where they were, so your hand does not have to find them again. InSidebarHeaderandSidebarFooter, mark your own text withdata-sidebar-labelso it fades with the rest.offcanvasslides it away completely, for pages that need the room.nonekeeps it open.
Peek
Rest the pointer on the rail and the whole sidebar opens over the page, with nothing underneath moving. Leave and it folds back. It waits a moment, so crossing the rail on your way somewhere does not open it. It stays open while a menu from inside it is open, and keyboard focus opens it too, so a keyboard user always sees what is focused.
This is why the rail has no tooltips: a peek shows every name at once, and sub-items as well.
Saved state
With storageKey, the collapsed state and the width are saved in the browser and put back on the next visit. Saved state usually needs a cookie your server reads, or the page flashes the default first. Here a small inline script restores it while the page is parsed, so there is no flash and nothing to wire up in your layout. With a strict Content Security Policy, pass its nonce.
Resizing
Drag the edge to change the width, between 192 and 384 pixels. Tab to it and use ← and → for the same. Set resizable={false} to turn it off. Resizing and the rail work together: the rail opens back to the width you chose.
Sub-items
SidebarSub is a row that opens to more rows. Mark it active when one of its items is the current page, and it starts open. On the rail its items wait for a peek.
<SidebarSub icon={<FunnelIcon />} label="Funnels" active>
<SidebarItem href="/funnels/signup" active>Signup</SidebarItem>
<SidebarItem href="/funnels/checkout">Checkout</SidebarItem>
</SidebarSub>Badges and actions
badge puts a count at the end of a row, and on the rail it becomes a dot on the icon. action adds a small button that appears on hover or focus, and SidebarGroup takes an action for its heading.
Offcanvas
Inset
variant="inset" sets the page on a card beside the sidebar.
Loading
SidebarItemSkeleton holds a row's place while the items load, at the same height, so nothing moves.
On phones
Below 768 pixels the sidebar leaves the page and becomes a sheet from the left. SidebarTrigger opens it, and picking an item closes it.
In your own code
useSidebar gives anything inside the provider the same state: collapsed, setCollapsed, toggle and mobile. collapsed and onCollapsedChange on the provider control it from outside.
Accessibility
SidebarContent is the page's navigation landmark, and SidebarInset its main landmark. The current page is marked with aria-current="page". On the rail the names stay in the page, so screen readers read every row. The trigger says whether the sidebar is open, and the edge is a separator with its width as a value.
| Key | Action |
|---|---|
| ⌘B / Ctrl+B | Collapse or expand. Skipped while typing, where it means bold |
| Tab | Moves through the items. On the rail, focus peeks it open |
| Esc | Closes a peek |
| ← / → | On the edge: narrower or wider |
Motion
The panel slides and the page glides beside it, using translate only: the page's width changes in one step and just its position eases, so long pages do not reflow on every frame. Labels fade. By keyboard it is instant, like everything that answers a key, and reduced motion keeps the fade alone.
Sidebar or something else?
API reference
useSidebar
The sidebar's state, from anywhere inside SidebarProvider.
SidebarProvider
Holds the state and lays out the sidebar and the page. Wrap both in it.
Prop
Type
Also takes every <div> attribute. Style it with [data-slot="sidebar-provider"].
--qui-sidebar-width: Its open width. Defaults to 15rem.--qui-sidebar-rail: The width of the collapsed rail.
Sidebar
The panel itself: a column on wide screens, and a sheet from the left on phones.
Also takes every <aside> attribute. Style it with [data-slot="sidebar"].
SidebarInset
The page beside the sidebar.
Also takes every <main> attribute. Style it with [data-slot="sidebar-inset"].
SidebarTrigger
Opens and closes the sidebar, and the sheet on phones.
Style it with [data-slot="sidebar-trigger"].
SidebarHeader
The top of the panel, for a logo or a workspace switcher. Stays in place while the content scrolls. Mark text with data-sidebar-label, and it fades on the rail the way an item's does.
Also takes every <div> attribute. Style it with [data-slot="sidebar-header"].
SidebarFooter
The bottom of the panel, for the signed-in user. data-sidebar-label works here as well.
Also takes every <div> attribute. Style it with [data-slot="sidebar-footer"].
SidebarContent
The part that scrolls, holding the groups. It is the page's navigation landmark.
Also takes every <nav> attribute. Style it with [data-slot="sidebar-content"].
SidebarGroup
A set of items, with an optional heading. On the rail the heading keeps its height, so icons do not jump.
Prop
Type
Also takes every <section> attribute. Style it with [data-slot="sidebar-group"].
SidebarItem
A link in the sidebar. Use render for your router's link, like render={<Link href="/" />}.
Prop
Type
Also takes every <a> attribute. Style it with [data-slot="sidebar-badge"].
SidebarSub
An item that opens to more items. Sub-items wait on the rail, and a peek shows them.
Prop
Type
Style it with [data-slot="sidebar-sub"].
SidebarItemSkeleton
A placeholder row while the items load, the same height as a real one.
Also takes every <li> attribute. Style it with [data-slot="sidebar-item-skeleton"].