Sidebar
Lays out an app's side navigation, with groups, a header and a footer. Collapses, and becomes a sheet on small screens.
Installation
If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.
When to use
Use it
- For the navigation of an app: its objects, pages or Setup sections, in groups with titles.
- For a list of destinations that stays on screen while the content changes, like Reports' folders or Object Manager's objects.
- For an app shell that collapses its navigation to make room and moves it into a sheet on small screens.
- For navigation inside one page, between the sections of a settings page or a folder list beside its content, with
collapsible="none", as the In a page example does.
Don't use it
- To switch between views of one record or page. Use Tabs.
- For a short list of links at the top of a page. Use Breadcrumb.
- For a menu of commands that opens from a button. Use Dropdown Menu.
- For a deep tree of nodes that expand in place, like Setup's object tree. Use Tree.
Usage
SidebarProvider
├── Sidebar
│ ├── SidebarHeader
│ ├── SidebarContent
│ │ └── SidebarGroup
│ │ ├── SidebarGroupLabel
│ │ └── SidebarGroupContent
│ │ └── SidebarMenu
│ │ └── SidebarMenuItem
│ │ ├── SidebarMenuButton
│ │ ├── SidebarMenuBadge
│ │ └── SidebarMenuSub
│ │ └── SidebarMenuSubItem
│ │ └── SidebarMenuSubButton
│ └── SidebarFooter
└── SidebarInset
└── SidebarTriggerEach SidebarMenuButton holds an icon and then its label in a span, last, so the label truncates.
import { HomeIcon } from "lucide-react"
import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"<SidebarMenuItem>
<SidebarMenuButton isActive aria-current="page">
<HomeIcon />
<span>Home</span>
</SidebarMenuButton>
</SidebarMenuItem>The examples hold the selected row in state, so a click selects it; an app derives the active item from its route and sets isActive and aria-current="page" on the current page's row.
collapsible on Sidebar sets how it closes: offcanvas slides it off screen, icon shrinks it to an icon rail, and none keeps it open at every width, with no sheet on small screens. A button's tooltip shows only on the icon rail. showOnHover on SidebarMenuAction hides the action until its row is hovered or focused, on screens 768px and wider.
Items are buttons. For an item that goes to a page, pass asChild with an <a> in Radix projects, or render={<a href="…" />} in Base UI projects, as the Links example does; SidebarMenuSubButton is a link already. In Radix projects, items with a tooltip need a TooltipProvider around the app.
The provider writes its state to a sidebar_state cookie but never reads it. Read the cookie on the server and pass it as defaultOpen, or the sidebar opens on every load.
The app shell's provider goes around the whole app, once. A page's own navigation gets its own SidebarProvider around just that region, with className="min-h-0" so it doesn't fill the screen, and a Sidebar with collapsible="none", which never collapses or moves into a sheet. It has no border of its own: add border-e border-sidebar-border on the side that meets the content, or variant="inset" for the shaded nav on a gray region. Every provider on the page listens for Ctrl+B and writes the same cookie. A collapsible="none" sidebar ignores its open state, so leave SidebarTrigger out of the region.
Examples
Links
Items that link to pages, through `asChild` on Radix or `render` on Base UI, the current page set with `isActive` and `aria-current="page"`.
In a page
Navigation between the sections of one page, beside their content, with `collapsible="none"` on `Sidebar` and `min-h-0` on the provider.
Shaded
The nav on the gray page beside a white content panel, set with `variant="inset"` on `Sidebar`.
Compact
Small rows for a long list in little vertical space, set with `size="sm"` on each `SidebarMenuButton`.
Quick Find
A `SidebarInput` in `SidebarHeader` that filters the items, as Setup's Quick Find.
Show more
A section that shows its first items and a Show more row that reveals the rest, as SLDS's overflow.
Nested items and actions
A `SidebarMenuSub` under an item, a `SidebarMenuAction` on rows, a `SidebarGroupAction` on a title, and a `SidebarSeparator`.
Icon rail
Collapsed with `collapsible="icon"` on `Sidebar`, each button naming itself with `tooltip`, under an app header and over a footer menu.
API reference
| Part | Built on |
|---|---|
SidebarProvider | <div> (opens in a new tab) |
Sidebar | <div> (opens in a new tab) |
SidebarTrigger | Button |
SidebarRail | <button> (opens in a new tab) |
SidebarInset | <main> (opens in a new tab) |
SidebarInput | Input |
SidebarHeader | <div> (opens in a new tab) |
SidebarFooter | <div> (opens in a new tab) |
SidebarSeparator | Separator |
SidebarContent | <div> (opens in a new tab) |
SidebarGroup | <div> (opens in a new tab) |
SidebarGroupLabel | <div> (opens in a new tab) |
SidebarGroupAction | <button> (opens in a new tab) |
SidebarGroupContent | <div> (opens in a new tab) |
SidebarMenu | <ul> (opens in a new tab) |
SidebarMenuItem | <li> (opens in a new tab) |
SidebarMenuButton | <button> (opens in a new tab) |
SidebarMenuAction | <button> (opens in a new tab) |
SidebarMenuBadge | <div> (opens in a new tab) |
SidebarMenuSkeleton | <div> (opens in a new tab) |
SidebarMenuSub | <ul> (opens in a new tab) |
SidebarMenuSubItem | <li> (opens in a new tab) |
SidebarMenuSubButton | <a> (opens in a new tab) |
SidebarProvider
| Prop | Type | Default |
|---|---|---|
defaultOpen | boolean | true |
open | boolean | |
onOpenChange | (open: boolean) => void |
Sidebar
| Prop | Type | Default |
|---|---|---|
side | "left" | "right" | "left" |
variant | "sidebar" | "floating" | "inset" | "sidebar" |
collapsible | "offcanvas" | "icon" | "none" | "offcanvas" |
SidebarGroupLabel
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Renders the child element in its place, with the props merged onto it. |
SidebarGroupAction
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Renders the child element in its place, with the props merged onto it. |
SidebarMenuButton
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Renders the child element in its place, with the props merged onto it. |
isActive | boolean | false | |
tooltip | string | React.ComponentProps<typeof TooltipContent> | ||
variant | "default" | "outline" | "default" | |
size | "default" | "sm" | "lg" | "default" |
SidebarMenuAction
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Renders the child element in its place, with the props merged onto it. |
showOnHover | boolean | false |
SidebarMenuSkeleton
| Prop | Type | Default |
|---|---|---|
showIcon | boolean | false |
SidebarMenuSubButton
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Renders the child element in its place, with the props merged onto it. |
size | "sm" | "md" | "md" | |
isActive | boolean | false |
useSidebarhook
Returns SidebarContextProps.
SidebarContextPropstype
| Member | Type | Default |
|---|---|---|
state* | "expanded" | "collapsed" | |
open* | boolean | |
setOpen* | (open: boolean) => void | |
openMobile* | boolean | |
setOpenMobile* | (open: boolean) => void | |
isMobile* | boolean | |
toggleSidebar* | () => void |
SLDS mapping
The shell is shadcn's. The list inside it is SLDS 2's vertical navigation, slds-nav-vertical.
SidebarGroupisslds-nav-vertical__sectionandSidebarGroupLabelisslds-nav-vertical__title.SidebarMenuButtonisslds-nav-vertical__action. Rows inSidebarHeaderorSidebarFooteraren't navigation, so they don't take its states.SidebarMenuBadgeisslds-badgeat the end of the row.SidebarMenuSubButtonis the same row indented one level, as SLDS's tree indents.
Known gaps:
- SLDS's nav doesn't collapse, float, nest or have a header or footer.
floating,collapsible="icon",SidebarRail,SidebarHeader,SidebarFooter,SidebarMenuAction,SidebarGroupAction,SidebarMenuSubandSidebarMenuSkeletonare shadcn's, built from the theme tokens. - SLDS's Show more row is a pattern, not a part. Build it from a
SidebarMenuButtonwitharia-expandedandaria-controlsafter the list, as the Show more example does. - Icons are lucide's, not SLDS's utility icons.
| Prop | Value | SLDS | Notes |
|---|---|---|---|
Sidebar variant | sidebar | slds-nav-vertical | The base look: a white nav with a border on its page side. |
Sidebar variant | inset | slds-nav-vertical_shade | SLDS's shaded nav: the nav sits on the gray page, the content is a white panel, and hovered and active rows are white, the active one with a line above and below. |
Sidebar variant | floating | none | shadcn's floating card, built from the tokens. |
Sidebar collapsible | offcanvas | none | The nav slides off screen. SLDS's vertical navigation doesn't collapse. |
Sidebar collapsible | icon | none | Collapses to an icon rail with tooltips. No SLDS counterpart. |
Sidebar collapsible | none | slds-nav-vertical | A fixed nav that never collapses, like Setup's, or a page's own navigation beside its content. |
SidebarMenuButton variant | default | slds-nav-vertical__action | |
SidebarMenuButton variant | outline | none | A bordered row on the container color. No SLDS counterpart. |
SidebarMenuButton size | default | slds-nav-vertical__action | SLDS's nav row. |
SidebarMenuButton size | sm | slds-nav-vertical_compact | SLDS's compact nav: shorter rows with less room at each end. A group's title tightens with its compact rows. |
SidebarMenuButton size | lg | none | A tall row for an avatar and two lines of text, as in a header or footer menu, with its avatar lined up with the section titles. |
SidebarMenuButton isActive | true | slds-is-active | The active bar, color and weight. Add aria-current="page" too. |
SidebarMenuSubButton size | md | none | SLDS has no nested items. The same row, its text one tree level past the parent's label. |
SidebarMenuSubButton size | sm | none | The indented row at compact height. |
Keyboard interactions
| Keys | Action |
|---|---|
| Ctrl+B, Cmd+B | Toggles the sidebar from anywhere on the page. |
| Escape | On a small screen, closes the sheet. On the icon rail, closes an open tooltip. |
Accessibility
- Wrap the groups in a
navwith anaria-labelso the navigation is a landmark.Sidebaritself is adiv. - Set
aria-current="page"next toisActiveon the current page's item.isActiveonly sets the look. - Say what a badge counts in the item's name.
SidebarMenuBadgesits beside the button, so its number isn't part of the name: put the count and its meaning in ansr-onlyspan inside the label'sspan(: 3 new items), and mark the badgearia-hidden, as the Reports navigation example does. - Give
SidebarMenuAction,SidebarGroupActionandSidebarInputanaria-label. - On the icon rail, each item's
tooltipnames it on hover and focus. Keep the label in the item too; it's the item's name. - On a small screen the sidebar is a modal dialog with its close button hidden. Escape, the backdrop and the trigger close it.
- In Lightning Blue,
SidebarMenuBadgehas 3.65:1 contrast, below WCAG AA's 4.5:1 for its text size. Salesforce's own badge has the same limitation.
Motion
- Collapsing and expanding slides the nav over 200ms, in every theme. On a small screen the sheet slides in as Sheet does.
- With reduced motion nothing slides. The sheet's backdrop still fades.
Responsive behavior
- The sidebar is 16rem wide, 3rem as an icon rail and 18rem in the sheet. Set
--sidebar-widthand--sidebar-width-iconon the provider'sstyleto change the first two. The sheet's width is fixed. - Below 768px the trigger opens a sheet instead. The breakpoint lives in the
use-mobilehook and in themd:classes; change both together. SidebarContentscrolls; the header and footer stay put.
Performance
- Every item renders up front. Hundreds are fine; for thousands, filter them as the Quick Find example does.
- On the server the desktop sidebar renders, hidden on a small screen, and the sheet takes over after hydration.
Rules
- Use
variant="inset"for the shaded nav andsize="sm"on the buttons for the compact nav. Don't restyle rows, bars or tints with utility classes. - Make an item a link when it goes to a page, and a button when it changes something on the page, as Show more does.
- Use
size="lg"for a row in the header or footer, so its avatar lines up with the section titles. - Use lucide icons as bare children. Don't import Salesforce icons into a primitive.