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 */}Configure <LeftNav> defaults application-wide.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
LeftNav: {
defaults: { width: 'md', collapsed: false },
},
},
});Configurable axes (LeftNavVariantProps):
| Axis | Type | Notes |
|---|---|---|
width | 'sm' | 'md' | 'lg' | Expanded-mode width. sm=w-48, md=w-60, lg=w-72. |
collapsed | boolean | Rail mode — icons only, labels visually hidden (still in DOM for a11y). |
Non-visual axes (items, activeId, brand, footer, linkComponent, ariaLabel, showCollapseToggle, event handlers) are per-instance — data / behavior — not theme-configurable.
Precedence chain (lowest → highest):
defaultVariants from the internal leftNavVariants recipe (width: 'md', collapsed: false).theme.components.LeftNav.defaults (application-wide).<LeftNav width="lg" />) — wins over theme.sx — appended to the root and wins over conflicting classes via tailwind-merge.TypeScript: theme.components.LeftNav.defaults autocompletes to the two axes above.
Reactivity: useComponentDefaults('LeftNav') subscribes to the theme store — patchTheme re-renders every mounted instance inheriting the changed axis.
| Prop | Type | Default | Description |
|---|---|---|---|
items | LeftNavNode[] | — | Nav items — a mix of LeftNavItem and LeftNavGroup. |
activeId | string | — | Currently-active item id (highlight + aria-current="page"). |
ariaLabel | string | 'Main navigation' | Visible label of the nav landmark. |
brand | ReactNode | — | Brand / logo node rendered at the top. |
collapsed | boolean | false | Rail mode — icons only, labels visually hidden but kept in DOM for a11y (sr-only). Group children collapse under a caret. |
footer | ReactNode | — | Footer node (e.g., user badge). |
linkComponent | LeftNavLinkComponent | 'a' | Override the link component. |
onCollapseChange | (collapsed: boolean) => void | — | Fired when the user clicks the collapse-toggle button. |
onGroupExpandedChange | (groupId: string, expanded: boolean) => void | — | Fired when a group's expanded state changes (controlled or not). |
showCollapseToggle | boolean | true | Show / hide the collapse toggle button. |
slotProps | LeftNavSlotProps | — | Per-slot className overrides. |
sx | string | — | Root className shortcut. |
width | 'md' | 'sm' | 'lg' | 'md' | Expanded-mode width tier — sm:w-48, md:w-60, lg:w-72. |
<LeftNav> is a compound component with 14 named slots. Each slot accepts a { className?: string } override via slotProps:
| Slot | Purpose |
|---|---|
root | Outer <nav> landmark. |
brand | Top slot containing the brand node (logo + name). |
list | The <ul> wrapping all nav items and groups. |
item | Each <li> wrapping a single nav item. |
itemLink | The clickable link/button of each item. |
itemActive | Applied additively when the item's id matches activeId. |
itemIcon | The leading icon container of an item. |
itemLabel | The text label of an item. |
itemBadge | The trailing badge on an item (unread count, etc.). |
group | Container wrapping a group and its children. |
groupHeader | The clickable header row of a group (label + caret). |
groupChildren | The nested <ul> of items inside an open group. |
footer | Bottom slot containing the footer node (user menu). |
collapseToggle | The rail-toggle button (showCollapseToggle). |
Use sx for a root-level class override; use slotProps for a specific inner region (e.g. itemActive primary color, groupHeader font-weight, brand padding).
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.