| name | compact-nav-menus |
| description | Build compact, space-efficient navigation sidebars, menus, and list panels using the project's established sizing tokens. Covers flat lists, grouped sections, two-tier expandable trees, and filter bars. Use when creating or refactoring sidebars, nav panels, settings menus, category lists, filter toolbars, or any UI that lists clickable items in a constrained-width column. Also use when a sidebar looks too wide, too padded, or wastes space. |
Compact Navigation & Menu Panels
Canonical sizing and spacing rules for navigation sidebars, list panels, and compact menus across the project. Every value below is a hard default — deviate only with justification.
Sizing Tokens
Sidebar Container
| Property | Value | Notes |
|---|
| Width | w-36 to w-44 | Never wider than w-44 (~176px). Use w-36–w-40 when labels are short. |
| Background | bg-muted/30 | Subtle tint, not solid bg-card. |
| Border | border-r border-border/60 | 60% opacity, not full border-border. |
| Visibility | hidden md:flex | Always hidden on mobile; use MobileDock or stacked list instead. |
| Scroll | Wrap content in <ScrollArea className="h-full w-full"> | Never overflow-y-auto on the aside itself. |
Nav Item — Top-Level (Flat or Parent)
| Property | Value |
|---|
| Font | text-xs (12px) |
| Padding | px-3 py-1.5 |
| Gap | gap-2 |
| Icon | h-3.5 w-3.5 |
| Active BG | bg-accent text-accent-foreground font-medium |
| Active icon | text-primary |
| Inactive | text-muted-foreground hover:text-foreground |
| Hover BG | hover:bg-muted/80 |
| Shape | No rounded corners on rows — full-bleed highlight. |
Nav Item — Child (Second Tier)
| Property | Value |
|---|
| Font | text-[11px] |
| Padding | pl-8 pr-3 py-1 |
| Gap | gap-1.5 |
| Icon | h-3 w-3 |
| Active | text-primary font-medium (no background) |
| Inactive icon | text-muted-foreground/60 |
Section Headers (Grouped Lists)
| Property | Value |
|---|
| Font | text-[10px] font-medium uppercase tracking-wider |
| Color | text-muted-foreground/70 |
| Padding | px-3 pt-3 pb-1 |
| Separator | border-t border-border/40 above the header, not <Separator> components. |
Filter Bar / Chip Row
| Property | Value |
|---|
| Container | px-3 py-1.5 flex items-center gap-1.5 flex-wrap |
| Chip (active) | px-2 py-0.5 rounded text-xs bg-primary text-primary-foreground |
| Chip (inactive) | px-2 py-0.5 rounded text-xs bg-muted/60 text-muted-foreground hover:bg-muted |
| Search input | h-7 text-xs pl-8 with icon at h-3.5 w-3.5 |
| Select dropdown | h-6 px-1.5 rounded text-xs bg-muted/60 border-0 |
Footer / Status Bar
| Property | Value |
|---|
| Padding | px-4 py-1.5 or px-4 py-2 |
| Font | text-[10px] or text-xs |
| Background | bg-muted/20 |
| Border | border-t border-border/60 |
| Buttons | h-7 text-xs gap-1 px-2.5 |
Anti-Patterns
These are the exact mistakes to fix when refactoring existing sidebars:
| Bad | Fix |
|---|
w-52, w-64, or wider | Reduce to w-36–w-44 |
text-sm on nav items | Use text-xs |
py-2, py-3 on nav rows | Use py-1.5 (top-level) or py-1 (child) |
gap-3 between icon and label | Use gap-2 (top-level) or gap-1.5 (child) |
h-4 w-4 icons in nav | Use h-3.5 w-3.5 (top-level) or h-3 w-3 (child) |
rounded-md on nav rows | Remove — use full-bleed row highlights |
bg-primary text-primary-foreground active | Use bg-accent text-accent-foreground |
p-3, p-4 wrapper padding inside sidebar | Use py-1 with no horizontal padding on the wrapper; let rows handle px-3 |
space-y-6 between groups | Use border-t border-border/40 separator + tight header |
<Separator> component | Replace with a border-t on the section header |
bg-card sidebar background | Use bg-muted/30 |
border-border (full opacity) | Use border-border/60 |
text-lg sidebar title | Remove header entirely or use text-xs uppercase section label |
Two-Tier Expandable Pattern
For sidebars where a top-level item reveals sub-items (like Settings > Preferences):
const isExpanded = isActive && !!item.children;
{}
<Link href={item.children ? `${item.href}?tab=${item.children[0].param}` : item.href}
className={cn(
'flex items-center gap-2 px-3 py-1.5 transition-colors text-xs',
'hover:bg-muted/80',
isActive ? 'bg-accent text-accent-foreground font-medium'
: 'text-muted-foreground hover:text-foreground',
)}
>
<span className={cn('shrink-0', isActive ? 'text-primary' : 'text-muted-foreground')}>
{item.icon}
</span>
<span className="truncate">{item.title}</span>
</Link>
{}
{isExpanded && (
<div className="py-0.5">
{item.children.map(child => (
<Link key={child.param}
href={`${item.href}?tab=${child.param}`}
className={cn(
'flex []',
'/',
? ' '
' ',
)}
>
{child.icon}
{child.title}
))}
)}
Grouped Sections Pattern
For sidebars with categorized groups (like a file manager):
<aside className="hidden md:flex w-40 shrink-0 border-r border-border/60 bg-muted/30">
<ScrollArea className="h-full w-full">
<nav>
{groups.map((group, i) => (
<div key={group.label}>
{/* Section header — acts as separator */}
<div className={cn(
'px-3 pt-3 pb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70',
i > 0 && 'border-t border-border/40',
)}>
{group.label}
</div>
{group.items.map(item => (
<Link key={item.href} href={item.href}
className={cn(
'flex items-center gap-2 px-3 py-1.5 transition-colors text-xs',
'hover:bg-muted/80',
isActive ? 'bg-accent text-accent-foreground font-medium'
: 'text-muted-foreground hover:text-foreground',
)}
>
{item.label}
))}
))}
</aside>
Refactoring Checklist
When converting an existing sidebar to this system:
- Container:
w-36–w-44, bg-muted/30, border-r border-border/60
- Remove wrapper padding: No
p-3/p-4 on the scroll content wrapper
- Items:
text-xs, px-3 py-1.5, gap-2, icons h-3.5 w-3.5
- Active state:
bg-accent text-accent-foreground font-medium (not bg-primary)
- No rounded rows: Remove
rounded-md from item classes
- Section separators: Replace
<Separator> and space-y-6 with border-t border-border/40 on headers
- Remove sidebar titles: Drop
text-lg font-semibold headers; use section labels if grouping is needed
- Icons: Lucide React only,
h-3.5 w-3.5 for top-level, h-3 w-3 for children
- ScrollArea: Wrap nav content in
<ScrollArea>, not overflow-y-auto
- Mobile:
hidden md:flex on the aside; provide MobileDock or stacked list alternative