Sidebar Navigation Implementation: Chat Prompts
The sidebar is the most-used navigation surface in your app. Every customer hits it dozens of times per session. Get it right and the app feels organized; get it wrong and users get lost in their own product.
The hard parts aren't placing links. They're: collapsible behavior across breakpoints, multi-level nested groups without losing structure, active-state highlighting with App Router (which deals you partial path matches and dynamic segments), workspace / org switcher integration, persistent collapsed state, mobile drawer transition, keyboard navigation + accessibility, badge counts on items, search-within-sidebar at large nav scale, and the user / settings cluster at the bottom.
This is the chat-prompt playbook for shipping a sidebar that doesn't get rewritten 3 months later.
When You Need a Real Sidebar (vs. a Top Nav)
Sidebar fits when:
- App has 8+ destination pages
- Hierarchical organization (workspaces > sections > pages)
- Power users; long sessions
- Content-area-dominant layout (sidebar is reference, not main UI)
- Settings, admin tools, dashboards
Top nav fits when:
- 3-7 destination pages
- Marketing-adjacent layouts
- Mobile-first products
- Content-heavy (the content is the product)
If both — many SaaS use sidebar in-product + top nav for marketing pages — make them feel like the same product.
Picking a Library
In 2026, three patterns dominate:
- shadcn/ui
sidebarcomponent — composable, customizable, the default for new Next.js apps - Radix Collapsible / NavigationMenu — primitives if you want to compose manually
- Hand-rolled with Tailwind — for highly custom layouts
I'm starting a new Next.js App Router app. I want to build a sidebar. Should I use shadcn/ui's sidebar, Radix primitives, or hand-roll?
Help me pick by:
- App scope: ~15 nav items, 2 levels of nesting, 1 workspace switcher, 1 user menu at bottom
- I want: collapsed state persistence, mobile drawer, keyboard nav, search within sidebar, active-state highlighting on App Router routes
- Stack: Next.js + Tailwind + shadcn/ui already installed
Make a recommendation and explain trade-offs.
Default answer for most teams: shadcn/ui's sidebar component. It composes Radix primitives and gives you sensible defaults out of the box. Drop into Radix only if you need behavior shadcn doesn't expose.
Basic Sidebar Layout
Build me a sidebar for my Next.js App Router SaaS using shadcn/ui's sidebar component.
Structure:
- Header: logo + workspace switcher
- Body: nav items grouped into sections
- "Workspace": Dashboard, Reports, Activity
- "Settings": Members, Billing, API Keys, Integrations
- Footer: User menu (avatar + name + dropdown to Settings / Sign out)
Behavior:
- Default expanded on desktop (>= 1024px)
- Collapsed icon-only mode toggleable via button
- Mobile: drawer that slides in from left (visible when hamburger menu tapped)
- Active item highlighted based on current pathname
- Persistent collapsed/expanded state via cookie (server-readable)
Stack: Next.js 16 App Router + shadcn/ui + Tailwind.
Scaffold:
- Layout file at `app/(app)/layout.tsx` wrapping pages
- `components/sidebar.tsx` component
- `components/sidebar-workspace-switcher.tsx`
- `components/sidebar-user-menu.tsx`
Show me the layout with proper App Router conventions and Server / Client component boundaries.
Active State on App Router
The trickiest part. App Router's usePathname() returns the current path; you have to decide what "active" means for each item.
My sidebar items have href patterns like:
- `/dashboard` (exact match)
- `/reports` (matches /reports, /reports/new, /reports/[id])
- `/settings/members` (matches /settings/members and any sub-routes)
- `/integrations/[id]` (dynamic; one per integration)
Build the active-state logic:
1. A `useActivePath(href)` hook that returns true if the current pathname should highlight this item
2. Logic:
- Exact match for top-level pages with no children
- "starts with" match for items that have sub-pages
- For dynamic patterns, use a custom matcher
I want a `<NavItem href="..." matchMode="exact" | "startsWith" | "custom">` API.
Implement the hook + the NavItem wrapper. Make sure it works with the Next.js Link component (no full-page reloads on click) and Server Components (the active check is client-side; the layout is server).
Stack: Next.js 16 App Router + TypeScript.
The pitfall: pathname.startsWith('/reports') matches /reports-archive too. Use pathname === href || pathname.startsWith(href + '/') for safer prefix matching.
Collapsible / Nested Sections
Nav items grouped into expandable sections.
I want some sidebar sections to be collapsible (a chevron toggles open/closed).
Behavior:
- Click section header → toggle expand/collapse
- Default expanded for the section containing the current active item
- Default collapsed for other sections (configurable per-section)
- Persist user's manual toggles (localStorage)
- Keyboard: Enter / Space to toggle when focused
- Screen reader: announce expanded/collapsed state via aria-expanded
Build:
- `<NavGroup label="..." defaultOpen={true|false} children />` component
- Auto-open if any child is the current active route
- Animation: simple height transition (avoid jarring snap)
- Indentation: child items 16px in from group header
Stack: shadcn/ui + Tailwind + Radix Collapsible primitive.
Workspace / Org Switcher
For multi-tenant apps. Click on the current workspace; popover with options to switch or create new.
Build the workspace switcher at the top of the sidebar:
1. Display: avatar + name of current workspace + chevron
2. Click: dropdown popover showing:
- List of all workspaces user belongs to
- Search input if 5+ workspaces
- "Switch to" → click switches; URL updates; layout reloads with new context
- "Create new workspace" CTA at bottom
- "Manage workspaces" link to settings
Data:
- Current workspace from server (set in layout via auth context)
- List of workspaces from `/api/workspaces` (cached via TanStack Query)
Switching mechanics:
- POST `/api/workspaces/[id]/switch` to set the cookie / session workspace ID
- After successful switch, navigate to `/dashboard` (or wherever default lands you for the new workspace)
- All workspace-scoped queries invalidated on switch
Implement with shadcn/ui Popover + Command (for search).
Stack: Next.js + TanStack Query + shadcn.
Collapsed (Icon-Only) Mode
When the user collapses the sidebar, only icons remain. Hovering shows the label as tooltip.
Add collapsed icon-only mode to my sidebar:
Visual:
- Width: ~64px when collapsed; ~240px when expanded
- Item rows: icon centered; label hidden
- Hovering an item: show label as tooltip (use Radix Tooltip)
- Section headers: hidden when collapsed (or shown as a thin separator)
- Workspace switcher: just the avatar; no name; click for full popover
- User menu: just the avatar at the bottom
Toggle:
- Button in sidebar header to expand/collapse
- Optional keyboard shortcut: `Cmd/Ctrl+/` toggles
- Preference persisted in cookie so server-rendering knows the state at SSR
Animation:
- 200-300ms width transition
- Don't animate label opacity if it causes flicker — just hide via display:none after transition
- Content area should resize smoothly with the sidebar
Implement.
Stack: Tailwind + Radix Tooltip + cookies for state.
The cookie-vs-localStorage decision: cookie persists across SSR (no flash of wrong state on initial load); localStorage requires a client-side effect that flashes. Use cookie.
Mobile Drawer
On small screens, sidebar becomes a drawer.
On mobile (< 1024px), the sidebar should be:
1. Hidden by default
2. Toggled by a hamburger button in the top bar
3. Slides in from left as a drawer (covers ~280px of width)
4. Backdrop dims the rest of the screen; click backdrop to close
5. Closes automatically on route change
Use Radix Dialog or Sheet primitive (shadcn/ui has Sheet).
Implement:
- A `<MobileSidebar />` component using Sheet
- Hamburger button visible only on mobile (CSS `lg:hidden`)
- The same nav items render in both desktop sidebar and mobile drawer (share via composition)
- Keyboard: Esc closes; trap focus inside while open
Stack: shadcn/ui Sheet + Tailwind.
Search Within Sidebar (cmd+k)
For apps with 30+ nav items.
My sidebar has too many items for users to scan. Add command-palette search:
Behavior:
- Cmd/Ctrl+K opens a command palette overlay
- Search across all nav items + recent pages + workspace items
- Fuzzy matching (use `cmdk` library or similar)
- Keyboard navigation (up/down arrows, Enter to select)
- Categories: "Navigation", "Recent", "Settings", etc.
- Shortcuts shown next to each command
This is in addition to the sidebar — not a replacement. The palette is for power users; sidebar is still primary.
Build with shadcn/ui Command (built on cmdk).
Stack: shadcn/ui Command + your nav data structure.
Badge Counts on Nav Items
Notifications counter on Inbox, mention counter on Tasks, etc.
Some nav items need badge counts:
- "Inbox" with unread count
- "Tasks" with assigned count
- "Mentions" with unread count
Behavior:
- Counts come from a real-time source (TanStack Query refetched on focus, or live via WebSocket)
- Badge color: blue for "new content"; red for "needs attention"
- Hide badge if count = 0
- Cap display at "99+" for very large counts
Build a `<NavItem badge={count} badgeColor="blue|red">` component.
Update strategy:
- TanStack Query with `refetchOnWindowFocus: true` and a 60s stale time as a baseline
- For real-time accuracy, integrate with your existing WebSocket / SSE channel
Stack: shadcn/ui + TanStack Query.
Keyboard Navigation & Accessibility
Sidebar must be fully usable via keyboard.
Make my sidebar accessible:
Keyboard:
- Tab moves focus to sidebar items in order
- Arrow up/down navigates within the sidebar (skips Tab to other regions)
- Enter activates the focused item
- Escape closes any open expandable section / popover
- Cmd/Ctrl+/ toggles collapsed
- Cmd/Ctrl+K opens search palette
Screen reader:
- Sidebar landmark: `<nav aria-label="Main navigation">`
- Each section: `<h2>` with section label (visually hidden if needed)
- Items announce their label and active state ("Dashboard, current page")
- Badges announced ("Inbox, 3 unread")
- Collapsible sections: aria-expanded on the trigger
- Mobile drawer: announces open/close state; focus trap inside
Color:
- Active items don't rely on color alone — also use weight + position indicator (left border accent)
- Focus ring is clearly visible, distinct from active
Build the accessible-ready version of my existing sidebar.
Persistent State (Server-Side Knowable)
User preferences (collapsed state, expanded sections) need to survive SSR without flash.
I want the sidebar's collapsed/expanded state and section-open state to be:
1. Persisted across reloads
2. Read on the server during SSR so the initial render is correct (no flash)
3. Updated client-side immediately on user action
Pattern: cookies, not localStorage.
Implement:
- A cookie `sidebar_collapsed=1|0` set when user toggles
- A cookie `sidebar_sections_open=workspace,settings` (CSV of open section IDs)
- Server-side: read cookies in the layout; pass initial state to the sidebar component as props
- Client-side: when state changes, update cookies via `document.cookie = ...` or via a Server Action
Show me:
- The layout reading cookies and passing initial state
- The client component receiving initial state and managing it via React state
- Cookie update on user action
Stack: Next.js 16 App Router.
Common Pitfalls
Active state matching with startsWith only. /reports highlights when on /reports-archive. Use pathname === href || pathname.startsWith(href + '/').
No SSR-aware initial state. localStorage-backed state flashes on initial load. Use cookies for state read on server.
Sidebar overflow. 30 nav items, no scrolling — items disappear under the fold. Make the body scrollable; pin header + footer.
No collapsed-mode tooltips. When collapsed, hovering an icon shows nothing. Always tooltip the label.
Mobile drawer doesn't close on route change. User taps a link; drawer stays open over the new page. Listen for route change and close.
Section auto-expand without animation. Sections snap open/closed jarringly. Animate height transitions.
Workspace switcher tied to React state without URL refresh. Switching workspace doesn't refresh data scoped to the old workspace. Invalidate all queries; navigate.
No keyboard nav. Tab works, but no arrow keys; sidebar needs explicit listeners.
Badge counts that flicker. Refetching every 5s with no transition causes the count to flash. Smoothly animate or debounce updates.
Settings cluster wrong. "Account" / "Settings" / "Billing" sometimes splits across the sidebar; sometimes all at the bottom; sometimes only in user menu. Pick a pattern; commit.
Excessive nesting. 4-level deep nav structure makes users hunt. Cap at 2 levels in sidebar; deeper structure goes inside individual pages.
Workspace name truncation. Long workspace names break the sidebar layout. Truncate with ellipsis + tooltip on hover.
No "back to dashboard" affordance. Sidebar disappears or hides during onboarding / settings; no clear "out" path. Keep the logo clickable to home.
Different sidebar across pages. Sidebar shows different items on /billing vs /dashboard. Confusing. Keep one canonical sidebar; let active state communicate location.
Forgetting to support the user's color scheme. Sidebar in light mode is fine; dark mode is broken. Test both.
Breakpoint awkwardness. Tablet (768-1024px) shows neither full sidebar nor mobile drawer cleanly. Decide: tablet shows expanded sidebar (desktop-like) or drawer (mobile-like). Pick one.
Nav badge counts queried per-render. Each render refetches counts. Use TanStack Query with proper stale times.
Logout button hidden behind a 3-click path. "Settings → Account → Sign Out" is annoying. Put Sign Out in user menu (1 click from avatar).
See Also
- Settings & Account Pages
- Workspace / Tenant Switcher
- Workspace Templates / Cloning
- Roles & Permissions
- Multi-Tenancy
- Keyboard Shortcuts & Command Palette
- Activity Feed / Timeline Implementation
- In-App Notifications
- Notification Preferences & Unsubscribe
- In-Product Release Notes / What's New
- Onboarding Tour Implementation
- Internationalization
- Dark Mode Implementation
- Empty States, Loading & Error States
- Toast Notifications UI
- Search & Autocomplete / Typeahead
- User Profile Pages
- Workspace Branding / Custom Domains / White-Label