Skip to content
Caseroom

Components · Navigation

Sidebar navigation

A docs sidebar: groups of links, the current page marked, collapsible groups on native details.

38% of the 2,062 sites in The UI Index have a navigation (781 sites).

Both previews render the same markup; the HTML tab below is that markup without a framework.

Install

React
npx shadcn@latest add https://www.themes.dev/caseroom/r/sidebar-nav.json
Astro
npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/sidebar-nav.json

Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS.

sidebar-nav.tsx
import { cn } from '@/lib/utils';
import { CHEVRON_PATH, sidebar, type SidebarGroup } from './sidebar-nav.variants';

export type { SidebarGroup };

export interface SidebarNavProps {
  groups: SidebarGroup[];
  /** The page you are on: its link gets aria-current=page. */
  currentPath?: string;
  label?: string;
  className?: string;
}

/** A docs sidebar: groups of links, the current page marked, collapsible groups on native details. */
export function SidebarNav({ groups, currentPath, label = 'Docs', className }: SidebarNavProps) {
  return (
    <nav aria-label={label} className={cn(sidebar.root, className)}>
      {groups.map((g) => {
        const list = (
          <ul className={sidebar.list}>
            {g.links.map((l) => (
              <li key={l.href}><a href={l.href} aria-current={l.href === currentPath ? 'page' : undefined} className={sidebar.link}>{l.label}</a></li>
            ))}
          </ul>
        );
        return g.collapsible ? (
          <details key={g.title} open={g.open} className={cn('group', sidebar.group)}>
            <summary className={sidebar.summary}>
              {g.title}
              <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={sidebar.chevron}><path d={CHEVRON_PATH} /></svg>
            </summary>
            {list}
          </details>
        ) : (
          <div key={g.title} className={sidebar.group}>
            <p className={sidebar.heading}>{g.title}</p>
            {list}
          </div>
        );
      })}
    </nav>
  );
}
sidebar-nav.variants.ts
/** A docs sidebar (ported from Flightdeck's docs-sidebar): groups of links under small headings, the current page
 *  marked with aria-current and a ground, collapsible groups on native details. */
export const sidebar = {
  root: 'w-full max-w-64 text-sm',
  group: 'mb-6 last:mb-0',
  heading: 'mb-1 px-2 text-xs font-medium text-muted-foreground',
  summary:
    'mb-1 flex min-h-8 cursor-pointer list-none items-center justify-between rounded-md px-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed [&::-webkit-details-marker]:hidden',
  chevron: 'size-3.5 transition-transform duration-150 group-open:rotate-180',
  list: 'm-0 grid list-none gap-0.5 p-0',
  link: 'flex min-h-8 items-center rounded-md px-2 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed aria-[current=page]:bg-accent aria-[current=page]:font-medium aria-[current=page]:text-foreground aria-[current=page]:hover:bg-input/30 aria-[current=page]:active:bg-pressed',
};

export type SidebarGroup = { title: string; links: { label: string; href: string }[]; collapsible?: boolean; open?: boolean };
export const CHEVRON_PATH = 'M4 6l4 4 4-4';