Monet
@etched/monet 0.0.1

Sidebar

Ported

The app's navigation: a nav beside the page. It collapses to an icon rail, and on phones it is a drawer (a popover) that opens on the current page's link and closes after a link is chosen. Native attributes and ref pass through.

Kumo source
import { ChartBarIcon, ChatsCircleIcon, GearIcon, GraphIcon, MagnifyingGlassIcon, TrayIcon } from "@phosphor-icons/react";
import { Avatar } from "@etched/monet/avatar";
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarProvider } from "@etched/monet/sidebar";

// A frame in place of the browser window. The provider takes its height, so the page scrolls inside it.
const frame = { blockSize: "32rem", overflow: "hidden", borderRadius: "var(--et-card-radius)", boxShadow: "0 0 0 1px var(--et-line)" };

// The page beside the sidebar. In an app, this is the page's `main`.
const page = { overflow: "auto", padding: "var(--et-space-6)" };

// Etched's mark, 28 px, as Orbit's header shows it.
function Logo() {
  return (
    <svg viewBox="0 0 116 116" fill="currentColor" aria-hidden="true" style={{ flex: "none", inlineSize: "var(--et-control-sm)", blockSize: "var(--et-control-sm)" }}>
      <path
        fillRule="evenodd"
        d="M0.456543 4.50079C0.456543 2.02536 2.4819 0 4.95733 0H111.476C113.952 0 115.977 2.02536 115.977 4.50079V111.02C115.977 113.495 113.952 115.52 111.476 115.52H4.95733C2.4819 115.52 0.456543 113.495 0.456543 111.02V4.50079ZM95.7545 96.8425H20.8667C19.9665 96.8425 19.2914 96.1671 19.2914 95.3419V85.9431C19.2914 85.118 20.0415 84.4429 20.8667 84.4429H83.3549C85.728 84.4429 88.4655 85.5681 90.1794 87.5184L96.7967 94.6953C97.6219 95.5204 96.9548 96.8425 95.7545 96.8425Z"
      />
    </svg>
  );
}

// Orbit's navigation, with Tracer as the current page.
function OrbitNav() {
  return (
    <>
      <SidebarHeader>
        <Logo />
        <span className="et-sidebar-label">Orbit</span>
      </SidebarHeader>
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel>Recruiting</SidebarGroupLabel>
          <SidebarMenu>
            <SidebarMenuButton href="#sidebar" icon={<GraphIcon />}>
              Topo
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<MagnifyingGlassIcon weight="fill" />} current>
              Tracer
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<TrayIcon />}>
              SOBO
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<ChatsCircleIcon />}>
              References
            </SidebarMenuButton>
          </SidebarMenu>
        </SidebarGroup>
        <SidebarGroup>
          <SidebarGroupLabel>Admin</SidebarGroupLabel>
          <SidebarMenu>
            <SidebarMenuButton href="#sidebar" icon={<GearIcon />}>
              Settings
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<ChartBarIcon />}>
              Data quality
            </SidebarMenuButton>
          </SidebarMenu>
        </SidebarGroup>
      </SidebarContent>
      <SidebarFooter>
        <Avatar name="Sam Lee" size="sm" alt="" />
        <span className="et-sidebar-label">Sam Lee</span>
      </SidebarFooter>
    </>
  );
}

// Orbit-style stroke icons, as its Python `icon()` writes them.
const icon = (paths: string) =>
  `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;

export function Preview() {
  return (
    <SidebarProvider style={{ ...frame, blockSize: "24rem" }}>
      <Sidebar aria-label="Orbit">
        <OrbitNav />
      </Sidebar>
      <div style={page} />
    </SidebarProvider>
  );
}

Installation

import { Sidebar, SidebarCollapsible, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarProvider, SidebarTrigger, useSidebar } from "@etched/monet/sidebar";

Examples

Groups and the current page

Group labels in sentence case, then links with an icon each. The current page has current: a filled row, strong text, and Phosphor's fill weight on its icon.

The page
import { ChartBarIcon, ChatsCircleIcon, GearIcon, GraphIcon, MagnifyingGlassIcon, TrayIcon } from "@phosphor-icons/react";
import { Avatar } from "@etched/monet/avatar";
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarProvider } from "@etched/monet/sidebar";

// A frame in place of the browser window. The provider takes its height, so the page scrolls inside it.
const frame = { blockSize: "32rem", overflow: "hidden", borderRadius: "var(--et-card-radius)", boxShadow: "0 0 0 1px var(--et-line)" };

// The page beside the sidebar. In an app, this is the page's `main`.
const page = { overflow: "auto", padding: "var(--et-space-6)" };

// Etched's mark, 28 px, as Orbit's header shows it.
function Logo() {
  return (
    <svg viewBox="0 0 116 116" fill="currentColor" aria-hidden="true" style={{ flex: "none", inlineSize: "var(--et-control-sm)", blockSize: "var(--et-control-sm)" }}>
      <path
        fillRule="evenodd"
        d="M0.456543 4.50079C0.456543 2.02536 2.4819 0 4.95733 0H111.476C113.952 0 115.977 2.02536 115.977 4.50079V111.02C115.977 113.495 113.952 115.52 111.476 115.52H4.95733C2.4819 115.52 0.456543 113.495 0.456543 111.02V4.50079ZM95.7545 96.8425H20.8667C19.9665 96.8425 19.2914 96.1671 19.2914 95.3419V85.9431C19.2914 85.118 20.0415 84.4429 20.8667 84.4429H83.3549C85.728 84.4429 88.4655 85.5681 90.1794 87.5184L96.7967 94.6953C97.6219 95.5204 96.9548 96.8425 95.7545 96.8425Z"
      />
    </svg>
  );
}

// Orbit's navigation, with Tracer as the current page.
function OrbitNav() {
  return (
    <>
      <SidebarHeader>
        <Logo />
        <span className="et-sidebar-label">Orbit</span>
      </SidebarHeader>
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel>Recruiting</SidebarGroupLabel>
          <SidebarMenu>
            <SidebarMenuButton href="#sidebar" icon={<GraphIcon />}>
              Topo
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<MagnifyingGlassIcon weight="fill" />} current>
              Tracer
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<TrayIcon />}>
              SOBO
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<ChatsCircleIcon />}>
              References
            </SidebarMenuButton>
          </SidebarMenu>
        </SidebarGroup>
        <SidebarGroup>
          <SidebarGroupLabel>Admin</SidebarGroupLabel>
          <SidebarMenu>
            <SidebarMenuButton href="#sidebar" icon={<GearIcon />}>
              Settings
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<ChartBarIcon />}>
              Data quality
            </SidebarMenuButton>
          </SidebarMenu>
        </SidebarGroup>
      </SidebarContent>
      <SidebarFooter>
        <Avatar name="Sam Lee" size="sm" alt="" />
        <span className="et-sidebar-label">Sam Lee</span>
      </SidebarFooter>
    </>
  );
}

// Orbit-style stroke icons, as its Python `icon()` writes them.
const icon = (paths: string) =>
  `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;

export function GroupsAndTheCurrentPage() {
  return (
    <SidebarProvider style={frame}>
      <Sidebar aria-label="Orbit">
        <OrbitNav />
      </Sidebar>
      <div style={page}>The page</div>
    </SidebarProvider>
  );
}

The icon rail

Collapsed, the sidebar shows only icons; hover or focus one for its name. The trigger widens it again; keep the choice with open and onOpenChange.

import { ChartBarIcon, ChatsCircleIcon, GearIcon, GraphIcon, MagnifyingGlassIcon, TrayIcon } from "@phosphor-icons/react";
import { Avatar } from "@etched/monet/avatar";
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarProvider, SidebarTrigger } from "@etched/monet/sidebar";

// A frame in place of the browser window. The provider takes its height, so the page scrolls inside it.
const frame = { blockSize: "32rem", overflow: "hidden", borderRadius: "var(--et-card-radius)", boxShadow: "0 0 0 1px var(--et-line)" };

// The page beside the sidebar. In an app, this is the page's `main`.
const page = { overflow: "auto", padding: "var(--et-space-6)" };

// Etched's mark, 28 px, as Orbit's header shows it.
function Logo() {
  return (
    <svg viewBox="0 0 116 116" fill="currentColor" aria-hidden="true" style={{ flex: "none", inlineSize: "var(--et-control-sm)", blockSize: "var(--et-control-sm)" }}>
      <path
        fillRule="evenodd"
        d="M0.456543 4.50079C0.456543 2.02536 2.4819 0 4.95733 0H111.476C113.952 0 115.977 2.02536 115.977 4.50079V111.02C115.977 113.495 113.952 115.52 111.476 115.52H4.95733C2.4819 115.52 0.456543 113.495 0.456543 111.02V4.50079ZM95.7545 96.8425H20.8667C19.9665 96.8425 19.2914 96.1671 19.2914 95.3419V85.9431C19.2914 85.118 20.0415 84.4429 20.8667 84.4429H83.3549C85.728 84.4429 88.4655 85.5681 90.1794 87.5184L96.7967 94.6953C97.6219 95.5204 96.9548 96.8425 95.7545 96.8425Z"
      />
    </svg>
  );
}

// Orbit's navigation, with Tracer as the current page.
function OrbitNav() {
  return (
    <>
      <SidebarHeader>
        <Logo />
        <span className="et-sidebar-label">Orbit</span>
      </SidebarHeader>
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel>Recruiting</SidebarGroupLabel>
          <SidebarMenu>
            <SidebarMenuButton href="#sidebar" icon={<GraphIcon />}>
              Topo
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<MagnifyingGlassIcon weight="fill" />} current>
              Tracer
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<TrayIcon />}>
              SOBO
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<ChatsCircleIcon />}>
              References
            </SidebarMenuButton>
          </SidebarMenu>
        </SidebarGroup>
        <SidebarGroup>
          <SidebarGroupLabel>Admin</SidebarGroupLabel>
          <SidebarMenu>
            <SidebarMenuButton href="#sidebar" icon={<GearIcon />}>
              Settings
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<ChartBarIcon />}>
              Data quality
            </SidebarMenuButton>
          </SidebarMenu>
        </SidebarGroup>
      </SidebarContent>
      <SidebarFooter>
        <Avatar name="Sam Lee" size="sm" alt="" />
        <span className="et-sidebar-label">Sam Lee</span>
      </SidebarFooter>
    </>
  );
}

// Orbit-style stroke icons, as its Python `icon()` writes them.
const icon = (paths: string) =>
  `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;

export function TheIconRail() {
  return (
    <SidebarProvider style={frame} defaultOpen={false}>
      <Sidebar aria-label="Orbit, collapsed">
        <OrbitNav />
      </Sidebar>
      <div style={page}>
        <SidebarTrigger />
      </div>
    </SidebarProvider>
  );
}

Sections that open

A SidebarCollapsible is a native details: its links sit indented under a line. Open it when it holds the current page.

import { GearIcon, ShieldCheckIcon, UsersIcon } from "@phosphor-icons/react";
import { Sidebar, SidebarCollapsible, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarProvider, SidebarTrigger } from "@etched/monet/sidebar";

// A frame in place of the browser window. The provider takes its height, so the page scrolls inside it.
const frame = { blockSize: "32rem", overflow: "hidden", borderRadius: "var(--et-card-radius)", boxShadow: "0 0 0 1px var(--et-line)" };

// The page beside the sidebar. In an app, this is the page's `main`.
const page = { overflow: "auto", padding: "var(--et-space-6)" };

// Orbit-style stroke icons, as its Python `icon()` writes them.
const icon = (paths: string) =>
  `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;

export function SectionsThatOpen() {
  return (
    <SidebarProvider style={frame}>
      <Sidebar aria-label="Settings">
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Admin</SidebarGroupLabel>
            <SidebarMenu>
              <SidebarCollapsible label="Settings" icon={<GearIcon />} defaultOpen>
                <SidebarMenuButton href="#sidebar">Users</SidebarMenuButton>
                <SidebarMenuButton href="#sidebar" current>
                  Notifications
                </SidebarMenuButton>
                <SidebarMenuButton href="#sidebar">Access</SidebarMenuButton>
              </SidebarCollapsible>
              <SidebarCollapsible label="Referrals" icon={<UsersIcon />}>
                <SidebarMenuButton href="#sidebar">Directory</SidebarMenuButton>
                <SidebarMenuButton href="#sidebar">Leaderboard</SidebarMenuButton>
              </SidebarCollapsible>
              <SidebarMenuButton href="#sidebar" icon={<ShieldCheckIcon />}>
                Data quality
              </SidebarMenuButton>
            </SidebarMenu>
          </SidebarGroup>
        </SidebarContent>
      </Sidebar>
      <div style={page}>
        <SidebarTrigger />
      </div>
    </SidebarProvider>
  );
}

Plain HTML

For pages without React, such as Orbit's Python templates. The look comes from classes and data-state only: here one sidebar is expanded and one collapsed. Below 48 rem, the popovertarget button opens the drawer without a script.

Expanded
Collapsed
// Orbit-style stroke icons, as its Python `icon()` writes them.
const icon = (paths: string) =>
  `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;

// The same sidebar from markup alone: no React, only the classes and attributes in sidebar.tsx's header.
const plainSidebar = (id: string, state: "expanded" | "collapsed") => `
<div class="et-sidebar-layout" style="block-size: 26rem; overflow: hidden; border-radius: var(--et-card-radius); box-shadow: 0 0 0 1px var(--et-line)">
  <nav class="et-sidebar" id="${id}" aria-label="Orbit, plain HTML, ${state}" popover data-state="${state}">
    <div class="et-sidebar-content">
      <div class="et-sidebar-group">
        <div class="et-sidebar-group-label">Recruiting</div>
        <ul class="et-sidebar-menu">
          <li class="et-sidebar-menu-item">
            <a class="et-sidebar-menu-button" href="#sidebar" aria-current="page" title="Tracer">${icon('<circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/>')}<span class="et-sidebar-label">Tracer</span></a>
          </li>
          <li class="et-sidebar-menu-item">
            <a class="et-sidebar-menu-button" href="#sidebar" title="SOBO">${icon('<path d="M3 13h5l2 3h4l2-3h5"/><path d="M5 5h14l2 8v6H3v-6z"/>')}<span class="et-sidebar-label">SOBO</span></a>
          </li>
        </ul>
      </div>
      <div class="et-sidebar-group">
        <div class="et-sidebar-group-label">Admin</div>
        <ul class="et-sidebar-menu">
          <li class="et-sidebar-menu-item">
            <details class="et-sidebar-collapsible" open>
              <summary class="et-sidebar-menu-button" title="Settings">${icon('<circle cx="12" cy="12" r="3"/><path d="M12 3v2m0 14v2M3 12h2m14 0h2"/>')}<span class="et-sidebar-label">Settings</span>${icon('<path d="m9 6 6 6-6 6"/>').replace("<svg", '<svg class="et-sidebar-chevron"')}</summary>
              <ul class="et-sidebar-menu">
                <li class="et-sidebar-menu-item"><a class="et-sidebar-menu-button" href="#sidebar"><span class="et-sidebar-label">Users</span></a></li>
                <li class="et-sidebar-menu-item"><a class="et-sidebar-menu-button" href="#sidebar"><span class="et-sidebar-label">Access</span></a></li>
              </ul>
            </details>
          </li>
        </ul>
      </div>
    </div>
  </nav>
  <div style="padding: var(--et-space-6)">
    <button class="et-sidebar-trigger" popovertarget="${id}" aria-label="Navigation">${icon('<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/>')}</button>
    ${state === "collapsed" ? "Collapsed" : "Expanded"}
  </div>
</div>`;

export function PlainHtml() {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(16rem, 1fr))", gap: "var(--et-space-5)" }}>
      <div dangerouslySetInnerHTML={{ __html: plainSidebar("plain-sidebar", "expanded") }} />
      <div dangerouslySetInnerHTML={{ __html: plainSidebar("plain-rail", "collapsed") }} />
    </div>
  );
}

App shell

The sidebar, then the page: a PageHeader with breadcrumbs, actions and tabs, and the content. The trigger beside the breadcrumbs collapses the sidebar; below 48 rem it opens the sidebar as a drawer.

Compiler Engineer

Toronto · Hiring manager: Sam Lee · 2 openings

NameStage
Ana RuizInterviewing
José NúñezScheduled
Lee ParkHired
import { ChartBarIcon, ChatsCircleIcon, GearIcon, GraphIcon, MagnifyingGlassIcon, PlusIcon, TrayIcon } from "@phosphor-icons/react";
import { Avatar } from "@etched/monet/avatar";
import { Breadcrumbs, BreadcrumbsCurrent, BreadcrumbsLink } from "@etched/monet/breadcrumbs";
import { Button } from "@etched/monet/button";
import { PageHeader } from "@etched/monet/page-header";
import { Pill } from "@etched/monet/pill";
import { Table } from "@etched/monet/table";
import { TabsNav } from "@etched/monet/tabs";
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarProvider, SidebarTrigger } from "@etched/monet/sidebar";

// A frame in place of the browser window. The provider takes its height, so the page scrolls inside it.
const frame = { blockSize: "32rem", overflow: "hidden", borderRadius: "var(--et-card-radius)", boxShadow: "0 0 0 1px var(--et-line)" };

// The page beside the sidebar. In an app, this is the page's `main`.
const page = { overflow: "auto", padding: "var(--et-space-6)" };

// Etched's mark, 28 px, as Orbit's header shows it.
function Logo() {
  return (
    <svg viewBox="0 0 116 116" fill="currentColor" aria-hidden="true" style={{ flex: "none", inlineSize: "var(--et-control-sm)", blockSize: "var(--et-control-sm)" }}>
      <path
        fillRule="evenodd"
        d="M0.456543 4.50079C0.456543 2.02536 2.4819 0 4.95733 0H111.476C113.952 0 115.977 2.02536 115.977 4.50079V111.02C115.977 113.495 113.952 115.52 111.476 115.52H4.95733C2.4819 115.52 0.456543 113.495 0.456543 111.02V4.50079ZM95.7545 96.8425H20.8667C19.9665 96.8425 19.2914 96.1671 19.2914 95.3419V85.9431C19.2914 85.118 20.0415 84.4429 20.8667 84.4429H83.3549C85.728 84.4429 88.4655 85.5681 90.1794 87.5184L96.7967 94.6953C97.6219 95.5204 96.9548 96.8425 95.7545 96.8425Z"
      />
    </svg>
  );
}

// Orbit's navigation, with Tracer as the current page.
function OrbitNav() {
  return (
    <>
      <SidebarHeader>
        <Logo />
        <span className="et-sidebar-label">Orbit</span>
      </SidebarHeader>
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel>Recruiting</SidebarGroupLabel>
          <SidebarMenu>
            <SidebarMenuButton href="#sidebar" icon={<GraphIcon />}>
              Topo
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<MagnifyingGlassIcon weight="fill" />} current>
              Tracer
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<TrayIcon />}>
              SOBO
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<ChatsCircleIcon />}>
              References
            </SidebarMenuButton>
          </SidebarMenu>
        </SidebarGroup>
        <SidebarGroup>
          <SidebarGroupLabel>Admin</SidebarGroupLabel>
          <SidebarMenu>
            <SidebarMenuButton href="#sidebar" icon={<GearIcon />}>
              Settings
            </SidebarMenuButton>
            <SidebarMenuButton href="#sidebar" icon={<ChartBarIcon />}>
              Data quality
            </SidebarMenuButton>
          </SidebarMenu>
        </SidebarGroup>
      </SidebarContent>
      <SidebarFooter>
        <Avatar name="Sam Lee" size="sm" alt="" />
        <span className="et-sidebar-label">Sam Lee</span>
      </SidebarFooter>
    </>
  );
}

// Orbit-style stroke icons, as its Python `icon()` writes them.
const icon = (paths: string) =>
  `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;

const candidates = [
  { name: "Ana Ruiz", stage: "Interviewing", tone: "cyan" },
  { name: "José Núñez", stage: "Scheduled", tone: "purple" },
  { name: "Lee Park", stage: "Hired", tone: "green" },
] as const;

export function AppShell() {
  return (
    <SidebarProvider style={{ ...frame, blockSize: "34rem" }}>
      <Sidebar aria-label="Orbit, app shell">
        <OrbitNav />
      </Sidebar>
      <div style={{ ...page, display: "grid", alignContent: "start", gap: "var(--et-space-6)" }}>
        <PageHeader
          breadcrumbs={
            <div className="row">
              <SidebarTrigger />
              <Breadcrumbs>
                <BreadcrumbsLink href="#sidebar">Tracer</BreadcrumbsLink>
                <BreadcrumbsCurrent>Compiler Engineer</BreadcrumbsCurrent>
              </Breadcrumbs>
            </div>
          }
          title="Compiler Engineer"
          description="Toronto · Hiring manager: Sam Lee · 2 openings"
          actions={
            <Button variant="primary" icon={<PlusIcon />}>
              Add candidate
            </Button>
          }
          tabs={
            <TabsNav
              aria-label="Role views"
              variant="underline"
              links={[
                { href: "#sidebar-pipeline", label: "Pipeline", current: true },
                { href: "#sidebar-summary", label: "Summary" },
              ]}
            />
          }
        />
        <div className="card">
          <Table aria-label="Compiler Engineer pipeline">
            <thead>
              <tr>
                <th>Name</th>
                <th>Stage</th>
              </tr>
            </thead>
            <tbody>
              {candidates.map(({ name, stage, tone }) => (
                <tr key={name}>
                  <th scope="row">{name}</th>
                  <td>
                    <Pill tone={tone}>{stage}</Pill>
                  </td>
                </tr>
              ))}
            </tbody>
          </Table>
        </div>
      </div>
    </SidebarProvider>
  );
}

Accessibility

  • Enter or Space on SidebarTrigger collapses or widens the sidebar. Enter on a section opens or closes it.
  • On a phone, the drawer opens with focus on the current page's link. Escape closes it, and focus goes back to the trigger.
  • A nav landmark named by the required aria-label. Items are links in lists; a section is a native details and summary.
  • The trigger is named "Navigation" and has aria-expanded. The current page's link has aria-current="page".
  • On the icon rail, the hidden text still names each link, and keyboard focus shows it in a tooltip.
  • Set current on the current page's link. In plain HTML, write aria-current="page", and let your script set data-state and the toggle's aria-expanded.

API reference

SidebarCollapsible

PropTypeDefaultDescription
label *ReactNode–The section's name.
iconReactNode–An icon before the name, for the icon rail.
defaultOpenbooleanfalseOpen at first. Open it when it holds the current page.
children *ReactNode–SidebarMenuButtons, without icons.

SidebarMenuButton

PropTypeDefaultDescription
iconReactNode–An icon before the text: it is all the icon rail shows. For the current page, Phosphor's fill weight.
currentboolean–The link to the page that shows now: aria-current="page", a filled row and strong text.
renderReactElement<{ className?: string }>–Renders another element with the item's props, class and children: a router link, render={<RouterLink to="/runs" />}, or a button for an action, render={<button type="button" onClick={…} />}.
tooltipReactNodethe childrenThe tooltip on the icon rail.

SidebarProvider

PropTypeDefaultDescription
defaultOpenbooleantrueWide at first, uncontrolled.
openboolean–Wide (true) or the icon rail (false), controlled. Keep the user's choice with it.
onOpenChange(open: boolean) => void–
children *ReactNode–A Sidebar, then the page.