Sidebar

The app's navigation, beside the page, collapsing to icons or out of view.

View as Markdown
Constella
Recents
Chats
<SidebarProvider>  <Sidebar collapsible="icon">    <SidebarHeader>      <SidebarExpandedOnly>…</SidebarExpandedOnly>      <SidebarHeaderActions>        <SidebarTrigger kbd={<Kbd>⌘B</Kbd>}>          <SidebarTriggerMark>            <Logo />          </SidebarTriggerMark>        </SidebarTrigger>      </SidebarHeaderActions>    </SidebarHeader>    <SidebarContent>      <SidebarMenu>        <SidebarMenuItem>          <SidebarMenuButton isActive tooltip="Chats">            <HugeiconsIcon icon={BubbleChatIcon} />            <SidebarMenuLabel>Chats</SidebarMenuLabel>          </SidebarMenuButton>        </SidebarMenuItem>      </SidebarMenu>    </SidebarContent>    <SidebarFooter>…</SidebarFooter>    <SidebarRail />  </Sidebar>  <SidebarInset>…</SidebarInset></SidebarProvider>

Installation

$ npx shadcn@latest add @constella/sidebar

Usage

import {  Sidebar,  SidebarContent,  SidebarInset,  SidebarProvider,  SidebarTrigger,} from "@/components/primitives/sidebar";

SidebarProvider holds the open state and toggles it on ⌘B or Ctrl+B with no animation. Put the Sidebar and a SidebarInset for the page inside it. Below 768px the sidebar opens as a Sheet instead.

<SidebarProvider>  <Sidebar>    <SidebarContent>…</SidebarContent>  </Sidebar>  <SidebarInset>    <SidebarTrigger />  </SidebarInset></SidebarProvider>

Put each row's text in a SidebarMenuLabel. A label too long for the row fades at its edge, and the row's tooltip shows it whole. As the sidebar narrows to its icons, the label fades out with the width.

Composition

SidebarProvider├── Sidebar│   ├── SidebarHeader│   │   ├── SidebarExpandedOnly│   │   └── SidebarHeaderActions│   │       └── SidebarTrigger│   │           └── SidebarTriggerMark│   ├── SidebarContent│   │   └── SidebarGroup│   │       ├── SidebarGroupLabel│   │       ├── SidebarGroupAction│   │       └── SidebarGroupContent│   │           └── SidebarMenu│   │               └── SidebarMenuItem│   │                   ├── SidebarMenuButton│   │                   │   ├── SidebarMenuLabel│   │                   │   └── SidebarMenuKbd│   │                   ├── SidebarMenuAction│   │                   ├── SidebarMenuBadge│   │                   └── SidebarMenuSub│   │                       └── SidebarMenuSubItem│   │                           └── SidebarMenuSubButton│   ├── SidebarFooter│   └── SidebarRail└── SidebarInset

SidebarHeader is a row the page header's height (48px), so the two line up. Put a brand or title first and SidebarHeaderActions last. SidebarTrigger collapses the sidebar, and in the mobile sheet closes it. Its kbd follows the label in its tooltip. Its icon is SidebarTriggerIcon, which picks the glyph for where it is; pass your own children to replace it. Wrap your logo in SidebarTriggerMark to show it in the icon rail until the trigger is pointed at or focused. Leave the trigger out of the sidebar when the page header has one, as with offcanvas.

Wrap anything that only fits the open sidebar in SidebarExpandedOnly. It stays while the width moves, so the edge passes over it, and is removed once the sidebar has narrowed to its icons. In the content it fades with the labels.

<SidebarHeader>  <SidebarExpandedOnly>    <Brand />  </SidebarExpandedOnly>  <SidebarHeaderActions>    <SidebarTrigger kbd={<Kbd>⌘B</Kbd>}>      <SidebarTriggerMark>        <Logo />      </SidebarTriggerMark>    </SidebarTrigger>  </SidebarHeaderActions></SidebarHeader>

Use isActive for the current page and tooltip to name the row in the icon rail, or when its label is cut off. Pass withEnd when the row has a SidebarMenuBadge, so its label leaves room for it.

Put a shortcut after the label in a SidebarMenuKbd. It shows while the row is pointed at or focused. In the rail, put it in the tooltip instead.

SizeHeight
sm28px
default32px
lg40px
<SidebarMenuButton  tooltip={    <>      New chat<Kbd>⌘O</Kbd>    </>  }>  <HugeiconsIcon icon={PencilEdit02Icon} />  <SidebarMenuLabel>New chat</SidebarMenuLabel>  <SidebarMenuKbd>    <Kbd>⌘O</Kbd>  </SidebarMenuKbd></SidebarMenuButton>

For an account menu, render a size="lg" SidebarMenuButton as a DropdownMenuTrigger. The row stays lit while its menu is open, and in the icon rail it narrows to its avatar.

<SidebarFooter>  <SidebarMenu>    <SidebarMenuItem>      <DropdownMenu>        <DropdownMenuTrigger          render={<SidebarMenuButton size="lg" tooltip="shadcn" />}        >          <Avatar>…</Avatar>          <SidebarMenuLabel>shadcn</SidebarMenuLabel>          <HugeiconsIcon icon={UnfoldMoreIcon} />        </DropdownMenuTrigger>        <DropdownMenuContent side="top">…</DropdownMenuContent>      </DropdownMenu>    </SidebarMenuItem>  </SidebarMenu></SidebarFooter>

Put a SidebarMenuAction after a row's button for an action of its own, like a menu. It's drawn over the row's end, and the label fades out before it. With showOnHover it shows only while its row is hovered or focused, or its menu is open, so the label keeps the full width the rest of the time. Show SidebarMenuSkeleton rows while the list loads.

Recents
Chats
<SidebarMenuItem>  <SidebarMenuButton isActive tooltip="Quarterly revenue summary">    <SidebarMenuLabel>Quarterly revenue summary</SidebarMenuLabel>  </SidebarMenuButton>  <DropdownMenu>    <DropdownMenuTrigger      render={<SidebarMenuAction aria-label="More" showOnHover />}    >      <HugeiconsIcon icon={MoreHorizontalIcon} />    </DropdownMenuTrigger>    <DropdownMenuContent side="right">…</DropdownMenuContent>  </DropdownMenu></SidebarMenuItem>

Sections

When a row leads into a section with its own sidebar, like Settings in the first demo, change it with changeSection from useSidebar. It runs your update in a view transition: the header and content slide forward into the section, or back out of it, while the trigger holds still and the page swaps at once.

const { changeSection } = useSidebar();
<SidebarMenuButton  onClick={() => {    changeSection("forward", () => setSection("settings"));  }}>  …</SidebarMenuButton>;

update can return a promise, like a router's navigate; the new section is captured when it settles. Only the provider that changed slides, so another sidebar on the page stays put.

RegionSlides by defaultChange it
SidebarHeaderYesswap={false}
SidebarContentYesswap={false}
SidebarFooterNoswap

Browsers without view transition types swap at once.

Side

Use side="right" to put the sidebar after the page. It collapses toward its own edge.

Chats
Constella
Recents
<SidebarProvider>  <SidebarInset>…</SidebarInset>  <Sidebar collapsible="icon" side="right">    …  </Sidebar></SidebarProvider>

Collapsible

Use the collapsible prop on Sidebar to choose how it closes. Collapsed to icons, a row shows its tooltip.

Constella
Recents
Chats
CollapsibleClosed
offcanvasOut of view
icon48px, its icons only
noneNever closes
<Sidebar collapsible="offcanvas">…</Sidebar>

API Reference

PartAdds
SidebarProvideropen, defaultOpen, onOpenChange; ⌘B / Ctrl+B
Sidebarside: "left" or "right"; collapsible: "offcanvas", "icon" or "none"
SidebarTriggerkbd; children default to SidebarTriggerIcon
SidebarTriggerMarkchildren: the mark shown in the rail
SidebarMenuButtonisActive, tooltip, withEnd; variant: "default" or "outline"; size: "sm", "default" or "lg"; render
SidebarMenuActionshowOnHover
SidebarMenuSkeletonshowIcon
SidebarHeader, SidebarContent, SidebarFooterswap
useSidebarstate, open, setOpen, openMobile, setOpenMobile, isMobile, toggleSidebar, changeSection(direction, update)

Every part takes sx, applied last.