macbetter

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

  • Sidebar is a navigation landmark. Name it with aria-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

PropTypeDefaultDescription
selection"accent" | "gray""accent"How the current row is filled.
aria-labelstring—Names the navigation.

SidebarSection

PropTypeDefaultDescription
titleReactNode—The header. A string also names the section's list.
collapsiblebooleanfalseMakes the header a disclosure button.
defaultOpenbooleantrueWhether a collapsible section starts open.
openboolean—Controls whether a collapsible section is open.
onOpenChange(open, details) => void—Runs when a collapsible section opens or closes.

SidebarItem

PropTypeDefaultDescription
hrefstring—Renders the row as a link. Without it, the row is a button.
currentbooleanfalseMarks the place on screen. Sets aria-current.
iconReactNode—A leading symbol, hidden from assistive technology.
badgeReactNode—A trailing count or status.
tintstring—A color for this row's icon.
onClickMouseEventHandler—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

NameOnDescription
data-selection.mb-sidebaraccent or gray.
data-current.mb-sidebar-itemThe current row.
data-open, data-closed.mb-sidebar-section-headerA collapsible section's state.
--mb-sidebar-bginheritedThe sidebar's background. Defaults to the sidebar material; a window sidebar and a split view's sidebar pane set it to transparent.
--mb-sidebar-filterinheritedThe sidebar's backdrop filter. Set to none alongside a transparent background.
--mb-sidebar-icon-color.mb-sidebar-itemSet by tint.
--mb-sidebar-row-heightinherited28 points on macOS, 44 on iPadOS.