Sidebar
A collapsible sidebar for app navigation with groups, nested items, active states and badges. When collapsed the labels are visually hidden but stay in the DOM for screen readers, with aria-current / aria-expanded semantics.
Installation
pnpm dlx @elyri/cli@latest add sidebarExamples
Basic
Header, grouped navigation and footer, with icons, badges and a disabled item; the controlled active value shows in the content area.
import { useState } from 'react';
import { Sidebar } from './components/elyri/Sidebar';
// 图标组件此处省略,可换成任意图标库
export function Example() {
const [active, setActive] = useState('home');
return (
<Sidebar active={active} onActiveChange={setActive} aria-label="主导航">
<Sidebar.Header>
<span className="brand">Elyri</span>
<Sidebar.Trigger />
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group label="工作区">
<Sidebar.Item value="home" icon={<HomeIcon />}>概览</Sidebar.Item>
<Sidebar.Item value="projects" icon={<FolderIcon />} badge={3}>项目</Sidebar.Item>
<Sidebar.Item value="inbox" icon={<InboxIcon />} badge="New">收件箱</Sidebar.Item>
</Sidebar.Group>
<Sidebar.Separator />
<Sidebar.Group label="账户">
<Sidebar.Item value="settings" icon={<CogIcon />}>设置</Sidebar.Item>
<Sidebar.Item value="billing" icon={<CardIcon />} disabled>账单</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Footer>
<span className="brand">Ada Lovelace</span>
</Sidebar.Footer>
</Sidebar>
);
}Collapse to icon rail
The Trigger in the header toggles between full width and an icon rail with an animated width.
import { Sidebar } from './components/elyri/Sidebar';
export function Example() {
return (
<Sidebar defaultActive="projects" aria-label="主导航">
<Sidebar.Header>
<span className="brand">Elyri</span>
<Sidebar.Trigger />
</Sidebar.Header>
<Sidebar.Content>
{/* 分组带图标,折叠成图标栏后仍能看到分组语义 */}
<Sidebar.Group label="工作区" icon={<FolderIcon />}>
<Sidebar.Item value="home" icon={<HomeIcon />}>概览</Sidebar.Item>
<Sidebar.Item value="projects" icon={<FolderIcon />} badge={3}>项目</Sidebar.Item>
<Sidebar.Item value="inbox" icon={<InboxIcon />}>收件箱</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar>
);
}Groups and nested items
Nest Sub / SubItem inside an Item for second-level navigation; the parent opens automatically when a child is active and responds to arrow keys.
import { Sidebar } from './components/elyri/Sidebar';
export function Example() {
return (
<Sidebar defaultActive="alpha" aria-label="主导航">
<Sidebar.Content>
<Sidebar.Group label="项目">
<Sidebar.Item value="all" icon={<FolderIcon />}>全部项目</Sidebar.Item>
<Sidebar.Item value="team" icon={<UsersIcon />}>
团队空间
<Sidebar.Sub>
<Sidebar.SubItem value="alpha">Alpha</Sidebar.SubItem>
<Sidebar.SubItem value="beta">Beta</Sidebar.SubItem>
<Sidebar.SubItem value="gamma" badge={2}>Gamma</Sidebar.SubItem>
</Sidebar.Sub>
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar>
);
}asChild with a router
With asChild the behaviour merges onto the only child, so you can drop in a react-router Link.
import { Link } from 'react-router-dom';
import { Sidebar } from './components/elyri/Sidebar';
export function Example() {
return (
<Sidebar defaultActive="docs" aria-label="主导航">
<Sidebar.Content>
<Sidebar.Group label="资源">
<Sidebar.Item value="docs" asChild icon={<BookIcon />} badge="9">
<Link to="/docs">文档</Link>
</Sidebar.Item>
<Sidebar.Item value="api" asChild icon={<CodeIcon />}>
<Link to="/api">API 参考</Link>
</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar>
);
}External trigger
Wrap the shell in SidebarProvider to lift the state out, then a button in the top bar calls useSidebar() to collapse or expand the sidebar.
import { Sidebar, SidebarProvider, useSidebar } from './components/elyri/Sidebar';
// 顶部导航栏里的按钮,位于 Sidebar 外部
function TopbarToggle() {
const { collapsed, toggleCollapsed } = useSidebar();
return (
<button type="button" onClick={toggleCollapsed}>
{collapsed ? '展开菜单' : '收起菜单'}
</button>
);
}
export function Example() {
return (
<SidebarProvider defaultActive="home">
<header className="topbar">
<span className="brand">Elyri</span>
<TopbarToggle />
</header>
<div className="shell">
<Sidebar aria-label="主导航">
<Sidebar.Content>
<Sidebar.Group label="工作区">
<Sidebar.Item value="home" icon={<HomeIcon />}>概览</Sidebar.Item>
<Sidebar.Item value="projects" icon={<FolderIcon />} badge={3}>项目</Sidebar.Item>
<Sidebar.Item value="inbox" icon={<InboxIcon />}>收件箱</Sidebar.Item>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar>
<main className="shell-main">当前页面</main>
</div>
</SidebarProvider>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
collapsed | boolean | — | Controlled collapsed state |
defaultCollapsed | boolean | false | Initial collapsed state when uncontrolled |
onCollapsedChange | (collapsed: boolean) => void | — | Called whenever the collapsed state changes |
collapsible | boolean | true | Whether collapsing is allowed; the Trigger is disabled when false |
active | string | — | Controlled active value |
defaultActive | string | '' | Initial active value when uncontrolled |
onActiveChange | (value: string) => void | — | Called on every active change, controlled or not |
SidebarProvider | component | — | Lifts the collapse and active state outside Sidebar so external buttons such as a top bar can share it; takes the same state props as Sidebar |
useSidebar() | hook | — | Reads collapsed / active and exposes toggleCollapsed; callable inside Sidebar or SidebarProvider |
Sidebar.Header | HTMLAttributes | — | Top area; only the Trigger remains when collapsed |
Sidebar.Content | HTMLAttributes | — | The scrollable navigation body, rendered as <nav> |
Sidebar.Group | HTMLAttributes | — | A group container with an optional label and icon |
Group.label | ReactNode | — | Group label, hidden when collapsed |
Group.icon | ReactNode | — | Group icon, kept when collapsed |
Sidebar.Item | HTMLAttributes | — | A nav item; becomes an expandable parent when it contains a Sub |
Item.value | string | — | Unique value matched against the active value on the root |
Item.icon | ReactNode | — | Leading icon |
Item.badge | ReactNode | — | Trailing badge or count |
Item.active | boolean | — | Force the active state, overriding the root active match |
Item.disabled | boolean | — | Disabled items cannot be activated |
Item.href | string | — | Render as an <a> link |
Item.asChild | boolean | false | Merge behaviour onto the only child (e.g. a router Link); leaf items only |
Sidebar.Sub | HTMLAttributes | — | The sub-navigation container; its open state comes from the parent Item |
Sidebar.SubItem | HTMLAttributes | — | A second-level item with the same props as Item |
Sidebar.Separator | HTMLAttributes | — | A divider |
Sidebar.Footer | HTMLAttributes | — | Bottom area; only the Trigger remains when collapsed |
Sidebar.Trigger | HTMLAttributes | — | The collapse toggle that flips collapsed |
Trigger.asChild | boolean | false | Skip the built-in button and merge behaviour onto the only child |
Trigger.expandLabel | string | — | Accessible name of the button while collapsed |
Trigger.collapseLabel | string | — | Accessible name of the button while expanded |