Displays a list of links or actions to navigate between different sections.
OUI exports 5 sidebar-related components:
import { Sidebar } from "@opengovsg/oui"<Sidebar
items={[
{ type: "header", children: "Section" },
{
startContent: <MailIcon />,
onPress: () => alert("Inbox clicked"),
children: "Inbox",
tooltip: "Inbox",
},
{
label: "Settings",
startContent: <Wrench />,
subItems: [
{
children: "Profile",
onPress: () => alert("Profile clicked"),
tooltip: "Profile",
},
],
},
]}
/>There are two ways to use the sidebar:
items array to the Sidebar component. This handles rendering and nesting automatically.SidebarRoot, SidebarItem, SidebarList, and SidebarHeader directly for full control over the structure.Link, Disclosure, Tooltip) with proper ARIA labels and keyboard navigation.SidebarList sections can independently expand and collapse, with support for both controlled and uncontrolled state.Sidebar component for quick setup, or compose individual components for custom layouts.Each SidebarItem is built on React Aria's Link component, so it accepts all Link props for navigation. Use href for standard URL navigation, or onPress for custom click handling.
// URL navigation
<SidebarItem href="/inbox" startContent={<MailIcon />}>
Inbox
</SidebarItem>
// Custom press handler
<SidebarItem onPress={() => alert("Inbox clicked")} startContent={<MailIcon />}>
Inbox
</SidebarItem>When using the data-driven Sidebar component, pass these props directly in the items array:
const items = [
{ children: "Inbox", href: "/inbox", startContent: <MailIcon /> },
{
children: "Starred",
onPress: () => navigate("/starred"),
startContent: <Star />,
},
]Use the size prop to change the density of the sidebar. The default size is md.
Medium (default)
Small
For full control over the sidebar structure, use the individual components directly instead of the items array.
import {
SidebarHeader,
SidebarItem,
SidebarList,
SidebarRoot,
} from "@opengovsg/oui"Use SidebarList items (or objects with subItems in the items array) to create expandable sections. Set defaultIsExpanded to control the initial expansion state.
By default, clicking anywhere on a SidebarList item toggles its expansion. Set onlyCaretToggle to true to restrict toggling to only the caret icon. This is useful when the section label itself should act as a navigable link.
When using
onlyCaretToggle, pass navigation props (such ashref) directly to theSidebarListcomponent. The label becomes aLinkelement while the caret remains a separate toggle button.
Use the isSelected prop on items to indicate the currently active navigation item. Top-level selected items receive a background highlight, while nested selected items display a left border accent instead.
isSelected accepts either a boolean or a () => boolean function, which is useful for dynamic route matching.
// Static
<SidebarItem isSelected>Active item</SidebarItem>
// Dynamic
<SidebarItem isSelected={() => pathname === "/inbox"}>Inbox</SidebarItem>Set isCollapsed to true to collapse the sidebar to show only icons. When collapsed:
endContent are hidden.tooltip prop for accessibility.Every item should have a
tooltipprop set when the sidebar supports collapsing. This ensures users can still identify each item when labels are hidden.
Use the tooltipProps and tooltipTriggerProps props on SidebarRoot (or Sidebar) to customise tooltip behavior globally for all items. These props are spread onto the underlying React Aria Tooltip and TooltipTrigger components respectively.
By default, collapsed sidebar tooltips use placement="right", offset={4}, and delay={0}.
Use isCollapsed and onCollapsedChange together for controlled collapse state. This lets you manage the collapsed state externally, for example with a toggle button.
On mobile layouts, you can render the sidebar inside a Modal to create a drawer-style navigation panel. Use classNames to override the modal's positioning so it slides in from the side.
<nav> element.<ul> list container.<li> element wrapping each item.<Link> element containing the item content.<li> element wrapping the section.Disclosure wrapper.DisclosurePanel containing nested items.<li> element wrapping the header.<h2> element.All components accept a classNames prop (via SidebarRoot) to override styles for specific slots:
<SidebarRoot
classNames={{
base: "custom-nav",
item: "custom-item",
label: "custom-label",
}}
>
{/* ... */}
</SidebarRoot>The Sidebar component is a high-level wrapper that generates items from a data array. It accepts all SidebarRoot props plus the following:
| Prop | Type | Default | Description |
|---|---|---|---|
items | GeneratedSidebarItem[] | - | Array of item definitions to render. |
Each item in the array can be one of three types:
Regular item – Rendered as SidebarItem:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | The item label. |
startContent | ReactNode | - | Content before the label (typically an icon). |
endContent | ReactNode | - | Content after the label. |
tooltip | string | - | Tooltip shown when the sidebar is collapsed. |
isSelected | boolean | () => boolean | - | Whether the item is currently selected. |
href | string | - | Navigation URL (from React Aria LinkProps). |
onPress | () => void | - | Press handler (from React Aria LinkProps). |
Header item – Rendered as SidebarHeader:
| Prop | Type | Default | Description |
|---|---|---|---|
type | "header" | - | Discriminator to indicate a header item. |
children | ReactNode | - | The header text. |
startContent | ReactNode | - | Content before the header text. |
endContent | ReactNode | - | Content after the header text. |
List item – Rendered as SidebarList:
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | - | The section label. |
subItems | GeneratedSidebarItem[] | - | Nested items (can contain any item type, including more lists). |
startContent | ReactNode | - | Content before the label. |
endContent | ReactNode | - | Content after the label. |
tooltip | string | - | Tooltip shown when the sidebar is collapsed. |
isSelected | boolean | () => boolean | - | Whether the section is currently selected. |
defaultIsExpanded | boolean | false | Initial expansion state (uncontrolled). |
isExpanded | boolean | - | Expansion state (controlled). |
onExpand | (isExpanded: boolean) => void | - | Handler called when expansion state changes. |
onlyCaretToggle | boolean | false | Only allow toggling via the caret icon. |
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "md" | The size variant. |
isCollapsed | boolean | - | Whether the sidebar is collapsed (controlled). |
defaultCollapsed | boolean | false | Whether the sidebar is collapsed by default. |
onCollapsedChange | (isCollapsed: boolean) => void | - | Handler called when the collapsed state changes. |
tooltipProps | Partial<TooltipProps> | - | Props spread onto each item's Tooltip when collapsed. |
tooltipTriggerProps | Partial<TooltipTriggerComponentProps> | - | Props spread onto each item's TooltipTrigger when collapsed. |
className | string | - | Custom class for the root <nav> element. |
classNames | SlotsToClasses<SidebarSlots> | - | Custom classes for individual slots. |
children | ReactNode | - | Sidebar content (items, headers, lists). |
Extends React Aria's Link props.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | The item label. |
startContent | ReactNode | - | Content before the label (typically an icon). |
endContent | ReactNode | - | Content after the label. |
tooltip | string | - | Tooltip shown when the sidebar is collapsed. |
isSelected | boolean | () => boolean | - | Whether the item is currently selected. |
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | - | The section label displayed in the trigger. |
children | ReactNode | - | Nested items rendered inside the collapsible panel. |
startContent | ReactNode | - | Content before the label. |
endContent | ReactNode | - | Content after the label. |
tooltip | string | - | Tooltip shown when the sidebar is collapsed. |
isSelected | boolean | () => boolean | - | Whether the section is currently selected. |
defaultIsExpanded | boolean | false | Initial expansion state (uncontrolled). |
isExpanded | boolean | - | Expansion state (controlled). |
onExpand | (isExpanded: boolean) => void | - | Handler called when expansion state changes. |
onlyCaretToggle | boolean | false | Restrict expand/collapse to only the caret icon. |
linkProps | LinkProps | - | Props for the link element (only used with onlyCaretToggle). |
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | The header text. |
startContent | ReactNode | - | Content before the header text. |
endContent | ReactNode | - | Content after the header text. |
The Sidebar component follows accessible navigation patterns:
Link and Disclosure components with proper ARIA semantics.<nav> for landmark navigation.aria-expanded to communicate their state.tooltip prop as aria-label to maintain screen reader accessibility.