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.
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>
);
}/** 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';---
import { cn } from '@/lib/utils';
import { CHEVRON_PATH, sidebar, type SidebarGroup } from './sidebar-nav.variants';
interface Props {
groups: SidebarGroup[];
/** The page you are on: its link gets aria-current=page. */
currentPath?: string;
label?: string;
class?: string;
}
const { groups, currentPath, label = 'Docs', class: className } = Astro.props;
---
<nav aria-label={label} class={cn(sidebar.root, className)}>
{
groups.map((g) => {
const list = (
<ul class={sidebar.list}>
{g.links.map((l) => (
<li><a href={l.href} aria-current={l.href === currentPath ? 'page' : undefined} class={sidebar.link}>{l.label}</a></li>
))}
</ul>
);
return g.collapsible ? (
<details open={g.open} class={cn('group', sidebar.group)}>
<summary class={sidebar.summary}>
{g.title}
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class={sidebar.chevron}><path d={CHEVRON_PATH} /></svg>
</summary>
{list}
</details>
) : (
<div class={sidebar.group}>
<p class={sidebar.heading}>{g.title}</p>
{list}
</div>
);
})
}
</nav>/** 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';<nav aria-label="Docs (example)" class="w-full max-w-64 text-sm">
<div class="mb-6 last:mb-0">
<p class="mb-1 px-2 text-xs font-medium text-muted-foreground">Get started</p>
<ul class="m-0 grid list-none gap-0.5 p-0">
<li>
<a href="#intro" class="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">
Introduction
</a>
</li>
<li>
<a href="#install" class="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">
Install
</a>
</li>
<li>
<a href="#first" class="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">
Your first deploy
</a>
</li>
</ul>
</div>
<details open class="group mb-6 last:mb-0">
<summary class="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">
Guides
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="size-3.5 transition-transform duration-150 group-open:rotate-180"><path d="M4 6l4 4 4-4"></path></svg>
</summary>
<ul class="m-0 grid list-none gap-0.5 p-0">
<li>
<a href="#domains" class="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">
Domains
</a>
</li>
<li>
<a href="#env" aria-current="page" class="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">
Environment variables
</a>
</li>
<li>
<a href="#previews" class="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">
Preview links
</a>
</li>
</ul>
</details>
<details class="group mb-6 last:mb-0">
<summary class="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">
Reference
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="size-3.5 transition-transform duration-150 group-open:rotate-180"><path d="M4 6l4 4 4-4"></path></svg>
</summary>
<ul class="m-0 grid list-none gap-0.5 p-0">
<li>
<a href="#cli" class="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">
CLI
</a>
</li>
<li>
<a href="#api" class="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">
API
</a>
</li>
</ul>
</details>
</nav>