MiHCM
navigation/@mihcm/ui·v0.21.0·stable

Main Sidebar

Figma-style main app sidebar. Compact 46px desktop rail, mobile bottom menu, expandable drilldown panel with icon, title, description, badges, nested levels, and custom panel slots. IconSidebar remains as a compatibility alias.

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

collapsible=icon
Expanded · click the « chevron in the header to collapse to icon rail

Content area. The active rail icon and current panel are reflected here.

Drilldown

Default · rail click reveals a panel that drills deeper · layout reflows

variant=drilldown
Drilldown · current level pushes back when you drill deeper

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

variant=floating
Floating · the panel never pushes the main content

Content area. The active rail icon and current panel are reflected here.

Miller columns

Each drill keeps the previous column visible · horizontally scrolling

variant=columns
Columns · click into any deeper level; previous columns stay

Content area. The active rail icon and current panel are reflected here.

Command-driven

Search-first · typing filters the whole tree · arrow keys + Enter

variant=command
Command · find any leaf by typing; breadcrumbs show the path

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

variant=hover
Hover · panel never pushes layout; great for dense workspaces

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.

MiHCM
Phone · 375 × 640

Tap the hamburger to slide the full sidebar over the content. The page area stays clear — no rail eats horizontal space.

Mobile · 375 px wide · hamburger → Sheet
MiHCM HR
Tablet 720 px
Tablet · 720 px

Same hamburger pattern at tablet sizes — content area stays full-width while the sidebar slides over on demand.

Tablet · 720 px wide · hamburger → Sheet

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

colorScheme
Graphite · pass any brand colour to colorScheme

Content area. The active rail icon and current panel are reflected here.

Plum + amber

bg · fg · accent · border re-tinted via colorScheme

colorScheme
Plum + amber · pass any brand colour to colorScheme

Content area. The active rail icon and current panel are reflected here.

Forest

bg · fg · accent · border re-tinted via colorScheme

colorScheme
Forest · pass any brand colour to colorScheme

Content area. The active rail icon and current panel are reflected here.

Bespoke whitelabel

bg · fg · accent · border re-tinted via colorScheme

every token
Bespoke whitelabel · pass any brand colour to colorScheme

Content area. The active rail icon and current panel are reflected here.

Controlled Main Sidebar

Content gets more breathing room in PageShell and examples.
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

The panel slides beside the rail and supports nested levels.
<MainSidebar
  items={ITEMS}
  activeKey={active}
  defaultExpanded
  onItemSelect={setActive}
  panelFooter={<Badge tone="success">Controlled drilldown</Badge>}
/>

Right-side rail

Content remains on the left. The clickable rail stays pinned to the right edge.
<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.

App header (mocked)
The trigger above drives 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).

In 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.

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"
/>