Browse docs
Browse docs
A sidebar navigation. Flat items, collapsible groups, optional rail (collapsed) mode where labels become sr-only and tooltips take over. Per-row RBAC means individual items hide/disable based on the user's role. Pairs naturally with <AppShell nav={...}>.
import { LeftNav } from '@dashforge/tw';
<LeftNav
items={[
{ id: 'home', label: 'Home', icon: <HomeIcon />, to: '/' },
{ id: 'projects', label: 'Projects', icon: <FolderIcon />, to: '/projects' },
{ id: 'settings', label: 'Settings', icon: <CogIcon />, to: '/settings' },
]}
activeId="home"
/>import { Link, useLocation } from 'react-router-dom';
const { pathname } = useLocation();
const activeId = pathname.split('/')[1] || 'home';
<LeftNav
linkComponent={Link}
activeId={activeId}
brand={<Brand />}
footer={<UserMenu />}
items={[
{ id: 'home', label: 'Home', icon: <HomeIcon />, to: '/' },
{ id: 'inbox', label: 'Inbox', icon: <InboxIcon />, to: '/inbox', badge: '3' },
{
id: 'projects',
kind: 'group',
label: 'Projects',
defaultExpanded: true,
children: [
{ id: 'p-active', label: 'Active', to: '/projects/active' },
{ id: 'p-archived', label: 'Archived', to: '/projects/archived' },
],
},
{ id: 'settings', label: 'Settings', icon: <CogIcon />, to: '/settings' },
]}
/>import { LeftNav } from '@dashforge/tw';
<LeftNav
activeId="inbox"
items={[
{ id: 'home', label: 'Home', href: '/' },
{ id: 'inbox', label: 'Inbox', href: '/inbox', badge: '3' },
{
id: 'projects',
kind: 'group',
label: 'Projects',
defaultExpanded: true,
children: [
{ id: 'p-active', label: 'Active', href: '/projects/active' },
{ id: 'p-archived', label: 'Archived', href: '/projects/archived' },
],
},
{ id: 'settings', label: 'Settings', href: '/settings' },
]}
/>items accepts a mix of LeftNavItem (flat link) and LeftNavGroup (expandable container):
const items = [
{ id: 'home', label: 'Home', to: '/' }, // LeftNavItem
{ id: 'docs', label: 'Docs', to: '/docs' }, // LeftNavItem
{
id: 'admin', kind: 'group', // LeftNavGroup
label: 'Admin',
children: [
{ id: 'users', label: 'Users', to: '/admin/users' },
{ id: 'roles', label: 'Roles', to: '/admin/roles' },
],
},
];const [collapsed, setCollapsed] = useState(false);
<LeftNav
items={ITEMS}
collapsed={collapsed}
onCollapseChange={setCollapsed}
showCollapseToggle // button at the bottom that flips collapsed
/>When collapsed, labels move to sr-only (screen-reader-only) and tooltips appear on hover — the icon-only rail. Width shrinks to ~64px.
<LeftNav
items={[
{ id: 'home', label: 'Home', to: '/' },
{
id: 'admin',
label: 'Admin',
to: '/admin',
access: { requires: 'workspace.admin', when: 'denied:hide' },
},
{
id: 'billing',
label: 'Billing',
to: '/billing',
access: { requires: 'billing.read', when: 'denied:disable' },
},
]}
/>Items with access denied are removed from the rendered nav (or disabled, depending on when). Groups also accept access.
{ id: 'inbox', label: 'Inbox', icon: <InboxIcon />, badge: '12' }
{ id: 'tasks', label: 'Tasks', badge: 'NEW' }
{ id: 'beta', label: 'Beta', badge: '•' } // dot indicator<LeftNav
brand={<Logo />}
footer={
<Stack direction="row" gap={2} align="center">
<Avatar src="..." />
<Stack gap={0}>
<Typography variant="body2">Jane</Typography>
<Typography variant="caption" color="muted">Admin</Typography>
</Stack>
</Stack>
}
items={ITEMS}
/>Both are optional. brand renders at the top above the items, footer pins to the bottom.
<LeftNav width="sm" items={ITEMS} /> {/* 200px */}
<LeftNav width="md" items={ITEMS} /> {/* 256px — default */}
<LeftNav width="lg" items={ITEMS} /> {/* 320px */}| Prop | Type | Default | Description |
|---|---|---|---|
items | LeftNavNode[] | — | Mix of LeftNavItem + LeftNavGroup. |
activeId | string | — | Id of the active item (highlighted). |
collapsed | boolean | false | Rail mode (controlled). |
onCollapseChange | (c: boolean) => void | — | Fired by the toggle. |
onGroupExpandedChange | (groupId, expanded) => void | — | Fired by group caret. |
brand | ReactNode | — | Top slot (logo + brand). |
footer | ReactNode | — | Bottom slot (user menu, version, etc.). |
linkComponent | ComponentType | 'a' | Router-agnostic Link. |
ariaLabel | string | 'Main navigation' | Nav landmark label. |
showCollapseToggle | boolean | true | Render the rail-mode toggle button. |
width | 'sm' | 'md' | 'lg' | 'md' | Expanded width. |
slotProps | LeftNavSlotProps | — | Per-slot overrides. |
sx | string | — | Utility-class override on root. |
root · brand · list · item · itemLink · itemActive · itemIcon · itemLabel · itemBadge · group · groupHeader · groupChildren · footer · collapseToggle
LeftNavItem / LeftNavGrouptype LeftNavItem = {
id: string;
label: ReactNode;
icon?: ReactNode;
badge?: ReactNode;
href?: string;
to?: string;
access?: AccessRequirement;
// …any extra props pass through to linkComponent
};
type LeftNavGroup = {
id: string;
kind: 'group';
label: ReactNode;
icon?: ReactNode;
defaultExpanded?: boolean;
expanded?: boolean; // controlled
access?: AccessRequirement; // group-level
children: LeftNavItem[];
};defaultExpanded). Pass expanded + onGroupExpandedChange for controlled state if you need to persist it across navigations.collapsed=true, labels become sr-only for AT but a tooltip on hover restores visibility for sighted users. Tooltip is built-in (no separate <Tooltip> import needed).id. For nested route matching (e.g. /projects/active → highlight the projects parent group too), compute the active id in your wrapper based on pathname.startsWith(...).href / to renders via linkComponent. Items without renders as <button type="button"> so you can wire onClick for action items (e.g. "Open Command Palette").<AppShell> — it handles the drawer transition for narrow viewports automatically. LeftNav doesn't ship a built-in drawer.