Sidebar
A sidebar of navigation: the team or product name at the top, grouped links with sub-items and badges, an optional footer, and the signed-in person at the bottom. It can fold down to icons, and comes in four looks. It is only the panel; DashboardShell adds the top bar and the phone menu.
A ready-made piece, built from the library's own parts.
A block puts several components together into something you would otherwise assemble by hand. It never fetches data: you give it the data, or answer its callbacks. It is copied into your project like any component, with the parts it uses, so you can change anything.
Basic
Dashboard
Key numbers
- Desktop
- Mobile
Recent customers
8 customers
Showing 1 to 5 of 8
Inset
Dashboard
Key numbers
- Desktop
- Mobile
Recent customers
8 customers
Showing 1 to 5 of 8
Floating
Dashboard
Key numbers
- Desktop
- Mobile
Recent customers
8 customers
Showing 1 to 5 of 8
Collapsed
Dashboard
Key numbers
- Desktop
- Mobile
Recent customers
8 customers
Showing 1 to 5 of 8
Standalone
In a sheet
Permissions
ClassNames
With submenus
Retention
Key numbers
- Desktop
- Mobile
Recent customers
8 customers
Showing 1 to 5 of 8
Icon rail
Overview
Key numbers
- Desktop
- Mobile
Recent customers
8 customers
Showing 1 to 5 of 8
With header and footer
Dashboard
Key numbers
- Desktop
- Mobile
Recent customers
8 customers
Showing 1 to 5 of 8
Grouped sections
Mentions
Key numbers
- Desktop
- Mobile
Recent customers
8 customers
Showing 1 to 5 of 8
Installation
npx rdloom add sidebarCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/sidebar.jsonWorks, but without upgrade tracking.
Usage
import {
BreadcrumbItem,
Breadcrumbs,
Button,
Chart,
ChartIcon,
CreditCardIcon,
CustomerTable,
DashboardPage,
DashboardShell,
EmptyState,
GlobeIcon,
HomeIcon,
InboxIcon,
SettingsIcon,
SparkleIcon,
UsersIcon,
findNavItem,
trailOf,
type Customer,
type NavGroup,
} from "@rdloom/react";
<DashboardPage
title="Dashboard"
description="How the business is doing this week."
actions={
<>
<Button variant="secondary">Export</Button>
<Button>New report</Button>
</>
}
stats={[
{ label: "Revenue", value: "$48,200", trend: { change: 12.5 }, summary: "Up on last week" },
{ label: "New customers", value: 1234, trend: { change: -4 }, summary: "A quiet week" },
{ label: "Active accounts", value: 45678, trend: { change: 8.1 }, summary: "Retention is steady" },
]}
>
<div className="rounded-2xl border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)] p-5 [box-shadow:var(--rd-elevation-raised)]">
<Chart
type="area"
title="Visitors"
description="Desktop and mobile, this week"
height={240}
data={{
labels: week,
series: [
{ name: "Desktop", values: [320, 410, 380, 520, 480, 360, 300] },
{ name: "Mobile", values: [210, 260, 300, 340, 390, 420, 380] },
],
}}
/>
</div>
<div className="flex flex-col gap-3">
<h2 className="text-lg font-semibold tracking-[-0.01em]">Recent customers</h2>
<CustomerTable customers={customers} insights="none" pageSize={5} locale="en-US" />
</div>
</DashboardPage>
);
}
// The sidebar is the left column of the shell. Items without an href call onNavigate; with an href they are real links.
export default function SidebarBasicExample() {
const [current, setCurrent] = useState("overview");
const [team, setTeam] = useState(teams[0]);
const page = findNavItem(navigation, current)!;
const trail = trailOf(navigation, current);
return (
<div className="h-full w-full">
<DashboardShell
navigation={navigation}
currentId={current}
onNavigate={(item) => setCurrent(item.id)}
team={team}
teams={teams}
onTeamChange={(t) => setTeam(teams.find((x) => x.id === t.id) ?? teams[0])}
user={{ name: "Ada Lovelace", email: "ada@example.com", menu: [{ id: "profile", label: "Your profile", onSelect: () => {} }, { id: "out", label: "Sign out", onSelect: () => {}, variant: "danger" }] }}
onSearch={() => {}}
sidebarAppearance="bordered"
header={
<Breadcrumbs label="You are here">
<BreadcrumbItem>{navigation.find((g) => g.items.some((i) => i.id === current || i.children?.some((c) => c.id === current)))?.label ?? "Platform"}</BreadcrumbItem>
<BreadcrumbItem>{trail[trail.length - 1] ?? page.label}</BreadcrumbItem>
</Breadcrumbs>
}
>
{current === "overview" ? (
<Overview />
) : (
<DashboardPage title={page.label}>
<EmptyState title={`${page.label} is yours to build`} description="Put any page here. The sidebar, the top bar and the way they fold on a phone are already done." />
</DashboardPage>
)}
</DashboardShell>
</div>API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
navigationrequiredThe navigation as groups of items. An item may hold one level of sub-items. An item may carry a permission ("allow", "disabled", "hidden" or { state, reason }): hidden items, and groups or parents left with nothing, are not drawn or counted; disabled items stay as focusable, non-navigating entries (aria-disabled) with the reason in a tooltip. | NavGroup[] | none |
currentIdThe id of the page you are on. It is marked as the current page (aria-current) and its parent opens. | string | none |
brandYour logo or product name at the top. Ignored when a team is given. | node | none |
teamThe organisation or workspace shown at the top, with its logo and a second line such as the plan. Replaces brand. | ShellTeam | none |
teamsOther workspaces to switch to. With two or more the team becomes a menu. | ShellTeam[] | none |
onTeamChangeCalled with the workspace the person chose. | (team: ShellTeam) => void | none |
userThe signed-in person: shown at the bottom with an account menu. | ShellUser | none |
footerContent above the account area, such as an upgrade card or a storage meter. It is hidden while the sidebar is folded. | node | none |
onSearchShows a search button under the name and calls this when it is pressed, e.g. to open a command palette. | () => void | none |
searchLabelText and accessible name of the search button. | string | "Search" |
labelAccessible name of the navigation. | string | "Main navigation" |
onNavigateCalled with the item when one is chosen. Items without an href use this to do their work. | (item: NavItem) => void | none |
renderLinkDraw items as your router's link. Return the link with the given className and children; call onClick when chosen. Without it, items with an href are plain links. | (props: { item: NavItem; className: string; children: ReactNode; isCurrent: boolean; onClick: () => void }) => ReactNode | none |
onPickCalled after any item is chosen. Use it to close a sheet that holds the sidebar on a phone. | () => void | none |
isCollapsedControlled: whether the sidebar is folded down to icons. | boolean | none |
defaultCollapsedStart folded down. | boolean | false |
onCollapsedChangeCalled when the person folds or opens the sidebar. Save it if you want it remembered. | (collapsed: boolean) => void | none |
collapsibleShow the Collapse button at the bottom. Turn it off when you fold the sidebar from somewhere else, such as a top bar. | boolean | true |
classNamesClass names for the parts of the sidebar, by slot name, added after the built-in ones so you can restyle one part without editing the file. Slots: root, header, team-switcher, search, nav, group, item, sub-list, footer, collapse, account. | Partial<Record<"root" | "header" | "team-switcher" | "search" | "nav" | "group" | "item" | "sub-list" | "footer" | "collapse" | "account", string>> | none |
appearancebordered: a line on the inner edge. subtle: a tinted panel with no line. floating: a rounded raised card with a margin around it. inset: no line and no fill, for a sidebar that sits on a tinted page beside an inset page card (DashboardShell draws that page card). | "bordered" | "subtle" | "floating" | "inset" | "bordered" |
Accessibility
Role navigation, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Tab: search button, items, Collapse button, then the account menu
- Enter or Space: follows a link or presses an item; on an item with sub-items, opens and closes its list
- In the account and workspace menus: arrow keys move, Enter chooses, Escape closes
Screen readers announce
- "Main navigation, navigation", then each item; the current one reads "Customers, current page"
- "Reports, button, collapsed" for an item with sub-items, "expanded" once opened
- "Collapse sidebar, button, expanded"
- "Account menu, Ada Lovelace, menu button"
What your code must do
- The links are inside a named navigation landmark
- The current page is marked with aria-current="page" and also shown by a tinted background, an accent-colored icon and a heavier label, never by color alone
- An item with sub-items is a button with aria-expanded and aria-controls; the sub-list is hidden when closed
- A badge is read after the label ("Inbox, 3") and drawn as a pill; in the folded sidebar it becomes a dot but is still read
- The folded sidebar keeps every item's name for assistive technology and shows it in a tooltip on hover and focus; the Collapse button says what it will do (Collapse sidebar, Expand sidebar) and has aria-expanded
- Width changes stop for visitors who prefer reduced motion
- The account button is named with the person's name; the menu is a real menu
- A workspace switcher is a menu button named with the current workspace; with only one workspace it is plain text
Block contract
- Data
- The navigation as plain data: groups of items, each with an id, label, optional href, icon, badge and sub-items.
- Data states
- None: it shows what it is given
- Permissions
- per navigation item
- Events
- onNavigate, onPick, onTeamChange, onSearch, onCollapsedChange
- You can replace
- appearance (bordered, subtle, floating); renderLink (your router's link); footer slot; --rd-sidebar-width and --rd-sidebar-rail
Guidelines
Use it when
- An admin, billing, analytics or internal tool with several sections
- You want only the navigation panel, inside your own layout
Avoid it when
- You also want the top bar, the page frame and the phone menu: use DashboardShell, which is built on this
- A marketing site or a one-page app: a top navigation bar is enough
Don't
- Putting a second navigation landmark with the same name on the page: give each one its own label
- Hiding the only way to a section behind the folded sidebar's tooltip: give every item an icon people can tell apart
- More than about 7 top-level items in one group without headings
- UI permission is not security: hiding or disabling an item only changes what people see, so the server must check access again for every request
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-surface-default--rd-color-surface-subtle--rd-color-surface-selected--rd-color-surface-raised--rd-color-border-default--rd-color-text-default--rd-color-text-muted--rd-color-action-primary--rd-color-focus-ring--rd-radius-control--rd-elevation-raised--rd-size-control-sm