Navigation
Sidebar
Move between an app's top-level places from a source list on Mac and iPad.
"use client"
import { Sidebar, SidebarItem, SidebarSection } from "macbetter/sidebar"
import { ArchiveIcon, FileIcon, InboxIcon, SendIcon, StarIcon, Trash2Icon, FolderIcon } from "lucide-react"
import { useState } from "react"
const mailboxes = [
{ name: "Inbox", icon: InboxIcon, count: 12 },
{ name: "VIP", icon: StarIcon },
{ name: "Drafts", icon: FileIcon, count: 2 },
{ name: "Sent", icon: SendIcon },
{ name: "Archive", icon: ArchiveIcon },
{ name: "Trash", icon: Trash2Icon },
]
const folders = ["Customers", "Investors", "Receipts"]
export default function Example() {
const [current, setCurrent] = useState("Inbox")
return (
<Sidebar
aria-label="Mailboxes"
style={{ width: 260, height: 420, borderRadius: 16, boxShadow: "var(--mb-glass-rim), var(--mb-shadow-menu)" }}
>
<SidebarSection title="Favorites">
{mailboxes.map(({ name, icon: Icon, count }) => (
<SidebarItem
key={name}
icon={<Icon />}
badge={count}
current={current === name}
onClick={() => setCurrent(name)}
>
{name}
</SidebarItem>
))}
</SidebarSection>
<SidebarSection title="macbetter" collapsible>
{folders.map((name) => (
<SidebarItem key={name} icon={<FolderIcon />} current={current === name} onClick={() => setCurrent(name)}>
{name}
</SidebarItem>
))}
</SidebarSection>
</Sidebar>
)
}When to use it
Use a sidebar for an app's top-level places: mailboxes, folders, playlists, projects. It stays visible while the content beside it changes, so people always know where they are and can go anywhere in one click.
- Group rows into a few sections with short headers. Let people collapse sections they don't use.
- Use a symbol for every row and keep labels to a word or two. A trailing count tells people what's waiting, like unread messages.
- Don't put actions in a sidebar. Commands belong in the toolbar or a menu.
On iPhone, use a tab bar or a list that pushes screens instead.
Usage
import { Sidebar, SidebarItem, SidebarSection } from "macbetter/sidebar"Rows with an href are links; rows without one are buttons, for apps that keep the place in state. Mark the place on screen with current.
Selection
The selected row fills with the accent color and its text and icon switch to the color that reads on the accent, as macOS does in the key window. Use selection="gray" for a quieter neutral fill that keeps the icon's color. Inside an inactive window, either style turns gray.
import { Sidebar, SidebarItem, SidebarSection } from "macbetter/sidebar"
import { ClockIcon, HeartIcon, ListMusicIcon, Music2Icon } from "lucide-react"
function Library({ selection }: { selection: "accent" | "gray" }) {
return (
<Sidebar
aria-label={selection === "accent" ? "Library, accent selection" : "Library, gray selection"}
selection={selection}
style={{ width: 220, borderRadius: 14, boxShadow: "var(--mb-glass-rim), var(--mb-shadow-menu)" }}
>
<SidebarSection title="Library">
<SidebarItem href="#recent" icon={<ClockIcon />}>
Recently Added
</SidebarItem>
<SidebarItem href="#songs" icon={<Music2Icon />} current>
Songs
</SidebarItem>
<SidebarItem href="#favorites" icon={<HeartIcon />} tint="var(--mb-pink)">
Favorites
</SidebarItem>
<SidebarItem href="#playlists" icon={<ListMusicIcon />} badge={8}>
Playlists
</SidebarItem>
</SidebarSection>
</Sidebar>
)
}
export default function Example() {
return (
<>
<Library selection="accent" />
<Library selection="gray" />
</>
)
}Collapsible sections
Add collapsible to a section and its header becomes a disclosure button. On the Mac the chevron appears when the pointer is over the section; on iPad it's always there, in the accent color.
Icon colors
Row icons take the accent color. Pass tint to color one row's icon, like a tag or a smart folder.
Platform behavior
- macOS: 28 point rows with 13 point labels, 16 point symbols and small, secondary section headers. Rows don't highlight on hover, as in Finder and Mail.
- iPadOS: 44 point rows with 17 point labels, bold section headers with a visible chevron, and larger symbols.
- In a window, place the sidebar in
WindowSidebar: it becomes an inset pane of glass and the sidebar drops its own material.
Accessibility
Sidebaris anavigationlandmark. Name it witharia-label, like "Mailboxes".- Each section is a list named by its header, so a screen reader announces "Favorites, list, 6 items".
- The current row has
aria-current="page". Its icon is decorative; the count is read after the label. - A collapsible header is a button with
aria-expanded, from ui-sdk's Collapsible. A collapsed section's rows leave the tab order.
API
Sidebar
| Prop | Type | Default | Description |
|---|---|---|---|
selection | "accent" | "gray" | "accent" | How the current row is filled. |
aria-label | string | — | Names the navigation. |
SidebarSection
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | The header. A string also names the section's list. |
collapsible | boolean | false | Makes the header a disclosure button. |
defaultOpen | boolean | true | Whether a collapsible section starts open. |
open | boolean | — | Controls whether a collapsible section is open. |
onOpenChange | (open, details) => void | — | Runs when a collapsible section opens or closes. |
SidebarItem
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | Renders the row as a link. Without it, the row is a button. |
current | boolean | false | Marks the place on screen. Sets aria-current. |
icon | ReactNode | — | A leading symbol, hidden from assistive technology. |
badge | ReactNode | — | A trailing count or status. |
tint | string | — | A color for this row's icon. |
onClick | MouseEventHandler | — | Runs when the row is chosen. |
A row with href takes the rest of an a's props; a row without it passes the same attributes, like id or data-*, to its button.
Data attributes and CSS variables
| Name | On | Description |
|---|---|---|
data-selection | .mb-sidebar | accent or gray. |
data-current | .mb-sidebar-item | The current row. |
data-open, data-closed | .mb-sidebar-section-header | A collapsible section's state. |
--mb-sidebar-bg | inherited | The sidebar's background. Defaults to the sidebar material; a window sidebar and a split view's sidebar pane set it to transparent. |
--mb-sidebar-filter | inherited | The sidebar's backdrop filter. Set to none alongside a transparent background. |
--mb-sidebar-icon-color | .mb-sidebar-item | Set by tint. |
--mb-sidebar-row-height | inherited | 28 points on macOS, 44 on iPadOS. |