Cadence Clinical
Patterns

Sidebar

Collapsible application navigation. Its items follow the density, the page shown is marked by shape as well as fill, and on a phone it is a sheet over the page.

GradeTested

Today

Four appointments are booked for this morning.

Usage

import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from "@cadence-clinical/ui";

<SidebarProvider>
  <Sidebar collapsible="icon">
    <SidebarContent>
      <SidebarGroup>
        <SidebarGroupLabel>Clinic</SidebarGroupLabel>
        <SidebarGroupContent>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton isActive tooltip="Today" render={<a href="/today" />}>
                <CalendarDays aria-hidden />
                <span>Today</span>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarGroupContent>
      </SidebarGroup>
    </SidebarContent>
  </Sidebar>
  <SidebarInset>
    <header>
      <SidebarTrigger />
    </header>
    {children}
  </SidebarInset>
</SidebarProvider>;

Wrap the whole layout in SidebarProvider: the Sidebar, and the SidebarInset that holds the page. The provider holds whether the sidebar is open, and useSidebar() reads and changes that state from anywhere inside it.

Render each SidebarMenuButton as a link through render, with an icon before its words. Set isActive on the item for the page that is shown.

Sidebar follows the shadcn Sidebar and keeps its part names, so a sidebar copied from a shadcn block works as written. Its sheet on a phone is built on the Base UI Dialog, and its tooltips on Tooltip.

For a whole page, with a header across the top and the sidebar beside the content, use the Application shell. Its inset layout is shadcn's sidebar-08, with the page's header inside the inset panel.

The parts

PartWhat it is
SidebarProviderThe state, and the layout of the sidebar beside the page.
SidebarThe sidebar itself.
SidebarHeader, SidebarFooterThe top and bottom, which do not scroll: a product name, the signed-in user's menu.
SidebarContentThe part that scrolls. It is a nav landmark.
SidebarGroup, SidebarGroupLabel, SidebarGroupContentA set of items with words above it.
SidebarGroupActionA button beside a group's label, such as "Add a list".
SidebarMenu, SidebarMenuItem, SidebarMenuButtonA list of items, one item, and its link or button.
SidebarMenuActionA button at the end of an item, such as the trigger of its menu.
SidebarMenuBadgeA count at the end of an item.
SidebarMenuSub, SidebarMenuSubItem, SidebarMenuSubButtonA list of links under an item. It is hidden when the sidebar is collapsed to icons.
SidebarInput, SidebarSeparatorA field, such as search, and a line between parts. The field is hidden when collapsed to icons.
SidebarTriggerThe button that opens and closes the sidebar.
SidebarRailA strip along the sidebar's edge that toggles it when pressed.
SidebarInsetThe page beside the sidebar, as main.

Give every icon-only control a name. SidebarGroupAction and SidebarMenuAction show an icon, so put their words in a span with sr-only.

Collapsing

collapsible on Sidebar sets what closing does:

  • offcanvas slides the sidebar off the edge. The page takes its room. This is the default.
  • icon narrows the sidebar to its icons. Each item keeps its words as its name, and its tooltip shows them beside it. A group whose items have no icons has nothing to show, so hide it with className="group-data-[collapsible=icon]:hidden".
  • none keeps it open, even on a phone. The trigger does nothing to it.

variant sets how it sits: sidebar is flush with the edge, floating stands off it on its own panel, and inset frames the page as a rounded panel on the sidebar's colour. side puts it on the left or the right.

The sidebar starts open. Set defaultOpen on the provider to start it closed, or control it with open and onOpenChange. The provider also remembers the choice in a sidebar_state cookie for seven days, under the name shadcn uses, so a server can read it and render the sidebar the way it was left.

The page that is shown

isActive fills the item, sets its words in medium weight and puts a bar at its start. The bar is 3px wide in the accent's text colour, which reaches 3:1 against the fill, so the item is marked by shape as well as colour. The item also carries aria-current="page", which a screen reader announces.

An inset sidebar with a breadcrumb

shadcn's sidebar-08 block builds from these parts and three other components. Each piece maps to a Cadence part:

  • The application at the top: a SidebarMenuButton with size="lg" in the SidebarHeader, with a square tile in bg-sidebar-primary and text-sidebar-primary-foreground holding the icon.
  • A list that opens to its sub-lists: a Collapsible rendered as the SidebarMenuItem, a CollapsibleTrigger rendered as a SidebarMenuAction, and a SidebarMenuSub in the CollapsibleContent. Name the trigger with words in a span with sr-only, such as "Team lists".
  • Secondary links at the bottom: a SidebarGroup with className="mt-auto" as the last group in SidebarContent, holding SidebarMenuButtons with size="sm".
  • The user's menu: a Dropdown menu whose trigger is a SidebarMenuButton with size="lg" in the SidebarFooter, holding the person's Avatar, their name and role, and a ChevronsUpDown icon. Hide the avatar with aria-hidden, because the name is beside it.
  • The page's header: the SidebarTrigger, a vertical Separator and a Breadcrumb whose pages above the one shown are hidden below md.

Use the Application shell's inset layout for the frame, so the page keeps its skip link and its main landmark. Storybook's Sidebar stories include the whole block as "Inset with breadcrumb".

On a phone

Below 768px wide, the sidebar is a sheet over the page. SidebarTrigger opens it. It holds focus while it is open, and Escape or a press outside closes it and returns focus to the trigger. A menu button does not close the sheet when it is pressed: close it yourself with setOpenMobile(false) from useSidebar() once the page has changed.

Colours

The sidebar has one colour token of its own, sidebar, its surface. The seven other shadcn sidebar tokens, such as sidebar-foreground and sidebar-primary, point at the Cadence token they correspond to, so they follow the mode, the contrast level, the accent and any brand without values of their own. The pairings the sidebar uses, including its text and focus ring on sidebar and the bar on the item's fill, are asserted in the @cadence-clinical/tokens test suite.

What Cadence changes

  • Items follow the density. An item is as high as a control: 32px compact and 44px comfortable. Its icon stays put as the sidebar collapses, because it sits as far from the item's edge as from its top. The collapsed sidebar is 48px wide compact and 68px comfortable. The shadcn item is 32px at every density.
  • The page shown is marked by a bar and aria-current, as well as the fill and the weight shadcn uses.
  • A long name wraps. shadcn cuts it off with an ellipsis.
  • Collapsed off the canvas, the sidebar's links are out of reach. They are hidden once the slide has finished, so Tab cannot move focus to links nobody can see. In shadcn they stay in the tab order.
  • Ctrl+B and Cmd+B leave fields alone. They toggle the sidebar, except while focus is in a field, where Ctrl+B means bold in a note. shadcn takes the keys everywhere.
  • The trigger says whether the sidebar is open, with aria-expanded. The rail is for a pointer only, so it is hidden from screen readers, where it would be a second control with the trigger's name.
  • SidebarContent is a nav landmark. If yours holds something else, pass render={<div />}.
  • An action set to showOnHover always shows on a touch screen. It hides until its item is hovered or focused only where there is a fine pointer. shadcn hides it on any screen 768px or wider, including a tablet that cannot hover.
  • onOpenChange works without open. In shadcn, passing it stops the sidebar changing unless you also control open.
  • No SidebarMenuSkeleton. Cadence has no Skeleton, and the shadcn skeleton picks a random width.

Accessibility

  • SidebarTrigger is named "Toggle sidebar" and states aria-expanded. Give it other words as its children.
  • Collapsed to icons, every item keeps its words as its accessible name. The tooltip shows them to a sighted pointer user, and does not open on a touch screen.
  • On a phone the sheet is a dialog named "Sidebar". It holds focus until it closes.
  • Name SidebarContent with aria-label when the page has another nav.

Props

PropOnTypeDefault
defaultOpenSidebarProviderbooleantrue
openSidebarProviderboolean
onOpenChangeSidebarProvider(open: boolean) => void
sideSidebar"left" | "right""left"
variantSidebar"sidebar" | "floating" | "inset""sidebar"
collapsibleSidebar"offcanvas" | "icon" | "none""offcanvas"
isActiveSidebarMenuButtonbooleanfalse
tooltipSidebarMenuButtonstring or TooltipContent's props
sizeSidebarMenuButton"sm" | "default" | "lg""default"
variantSidebarMenuButton"default" | "outline""default"
showOnHoverSidebarMenuActionbooleanfalse
isActiveSidebarMenuSubButtonbooleanfalse
sizeSidebarMenuSubButton"sm" | "md""md"

On this page