In context
Five interaction models share the same MainSidebarItem tree. Pick the one that matches your shell — drilldown for classic app navigation, floating to keep content under the panel, columns for spatial drill-down power users, command for keyboard-first navigation, hover to keep the rail compact forever.
Expanded ↔ icon toggle
shadcn-style · wide sidebar with icon + label by default · collapses to the icon rail on toggle
Content area. The active rail icon and current panel are reflected here.
Drilldown
Default · rail click reveals a panel that drills deeper · layout reflows
Content area. The active rail icon and current panel are reflected here.
Floating panel
Overlay panel that floats over content · dismiss on outside click or Esc
Content area. The active rail icon and current panel are reflected here.
Miller columns
Each drill keeps the previous column visible · horizontally scrolling
Content area. The active rail icon and current panel are reflected here.
Command-driven
Search-first · typing filters the whole tree · arrow keys + Enter
Content area. The active rail icon and current panel are reflected here.
Hover-reveal
Stays in icon rail · hovering an icon pops out a floating mini-menu
Content area. The active rail icon and current panel are reflected here.
Responsive · mobile + tablet
The same sidebar rendered inside phone and tablet viewport frames so you can sanity-check touch behaviour, rail tap targets, panel overlay, and outside-tap dismissal at those sizes.
Tap the hamburger to slide the full sidebar over the content. The page area stays clear — no rail eats horizontal space.
Same hamburger pattern at tablet sizes — content area stays full-width while the sidebar slides over on demand.
Whitelabel theming · colorScheme
Pass any brand colours to the colorScheme prop — rail, expanded panel, active highlight, panel surface, breadcrumbs, badges, tooltips and dividers all re-tint from the same set of tokens. No className overrides required.
Graphite
bg · fg · accent · border re-tinted via colorScheme
Content area. The active rail icon and current panel are reflected here.
Plum + amber
bg · fg · accent · border re-tinted via colorScheme
Content area. The active rail icon and current panel are reflected here.
Forest
bg · fg · accent · border re-tinted via colorScheme
Content area. The active rail icon and current panel are reflected here.
Bespoke whitelabel
bg · fg · accent · border re-tinted via colorScheme
Content area. The active rail icon and current panel are reflected here.
Controlled Main Sidebar
import { useState } from 'react';
import { MainSidebar, type MainSidebarItem } from '@mihcm/ui/MainSidebar';
const ITEMS: MainSidebarItem[] = [
{
key: 'people',
label: 'People',
icon: <Users />,
children: [
{ key: 'directory', label: 'Directory', icon: <Users /> },
{ key: 'movement', label: 'Movement requests', icon: <FileText /> },
],
},
{ key: 'security', label: 'Security', icon: <Shield /> },
];
const [active, setActive] = useState('security');
<MainSidebar items={ITEMS} activeKey={active} onItemSelect={setActive} />;Expanded drilldown panel
<MainSidebar
items={ITEMS}
activeKey={active}
defaultExpanded
onItemSelect={setActive}
panelFooter={<Badge tone="success">Controlled drilldown</Badge>}
/>Right-side rail
<MainSidebar
side="right"
items={ITEMS}
activeKey={active}
defaultExpanded
onItemSelect={setActive}
/>Controlled mobile — open from an external trigger
Pass mobileOpen + onMobileOpenChange to drive the mobile Sheet from a trigger that lives outside the sidebar (typically TopBarMenuButton). The built-in hamburger is suppressed automatically.
mobileOpendirectly — MainSidebar's built-in hamburger is suppressed so there's no duplicate.import { useState } from 'react';
import { TopBar, TopBarLogo, TopBarMenuButton } from '@mihcm/ui/TopBar';
import { MainSidebar } from '@mihcm/ui/MainSidebar';
function AppShell() {
const [navOpen, setNavOpen] = useState(false);
return (
<>
<TopBar
start={
<>
<TopBarMenuButton onClick={() => setNavOpen(true)} />
<TopBarLogo />
</>
}
/>
<MainSidebar
mobile
items={ITEMS}
mobileOpen={navOpen}
onMobileOpenChange={setNavOpen}
/>
</>
);
}Uncontrolled usage (no mobileOpen) keeps the previous behaviour — the sidebar manages open state internally and renders its own hamburger.
Push layout — panel reflows page content
layoutMode="push" makes the expanded panel a flex sibling of the rail instead of an absolute overlay. The sidebar's effective width grows when the panel opens, and adjacent page content reflows to the right — best for Notion / Linear-style navigation where the page must remain fully visible. Outside-click dismissal is a no-op in push mode (panel is not modal).
layoutMode="push" the expanded panel is a flex sibling of the rail, so this content column reflows when the panel opens instead of being covered. Best for Notion / Linear-style navigation where the page must remain fully visible.<MainSidebar
items={ITEMS}
activeKey={active}
defaultExpanded
layoutMode="push"
onItemSelect={setActive}
/>layoutMode is supported by drilldown (default) and columns. The floating, command, and hover variants intentionally remain overlay-only — their interaction models depend on the panel floating.
Link mode
const ITEMS: MainSidebarItem[] = [
{ key: 'home', label: 'Home', icon: <Home />, href: '/' },
{ key: 'security', label: 'Security', icon: <Shield />, href: '/security' },
];
<MainSidebar items={ITEMS} activeKey={pathname.split('/')[1] || 'home'} />;Customization
<MainSidebar
items={ITEMS}
expanded={isExpanded}
onExpandedChange={setExpanded}
panelHeader={<ProductSwitcher />}
panelFooter={<UserHelp />}
railClassName="bg-primary"
panelClassName="bg-popover"
itemClassName="focus-visible:ring-primary-foreground"
activeItemClassName="bg-accent"
/>