Sidebar nav
Sidebar links in sections with an eyebrow heading, optional icons and mono counts.
npx shadcn@latest add https://ui.goodtabs.co/r/sidebar-nav.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/sidebar-nav
Two titled sections with counts, one item active, inside a 256px box.
With icons
A 16px icon before each label, a flat section above a titled one.
Flat list
One section without a title at a fixed width, no box.
Usage
import { SidebarNav } from "@/components/ui/sidebar-nav"
<SidebarNav
width="16rem"
sections={[
{
title: "Library",
items: [
{ label: "Inspiration", href: "/inspiration", count: 128 },
{ label: "Components", href: "/components", count: 64, active: true },
],
},
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| sections | { title?: string; items: { label: string; href: string; icon?: ReactNode; count?: number | string; active?: boolean }[] }[] | Sections in order. A section without a title is a flat list. | |
| width | string | CSS width, for example "16rem". Fills its container by default. |
Source
sidebar-nav/sidebar-nav.tsx
import * as React from "react";
import Link from "next/link";
import { cn } from "@/lib/utils";
export type SidebarNavItem = {
label: string;
href: string;
/** 16px icon before the label. */
icon?: React.ReactNode;
/** Mono count on the right. */
count?: number | string;
/** Marks the current page. Sets aria-current and the filled state. */
active?: boolean;
};
export type SidebarNavSection = {
/** Mono uppercase eyebrow above the items. Omit for a flat list. */
title?: string;
items: SidebarNavItem[];
};
type SidebarNavProps = Omit<React.ComponentProps<"nav">, "children"> & {
/** Sections in order. One section without a title is a flat list. */
sections: SidebarNavSection[];
/** CSS width, for example "16rem". Fills its container by default. */
width?: string;
};
/**
* Sidebar links in sections: an eyebrow heading, then 32px items with an optional icon and count.
* Server component. Set active yourself from the pathname.
*/
export function SidebarNav({ sections, width, className, style, ...props }: SidebarNavProps) {
const id = React.useId();
return (
<nav
data-slot="sidebar-nav"
aria-label="Sidebar"
className={cn("flex flex-col gap-5", className)}
style={{ width, ...style }}
{...props}
>
{sections.map((section, s) => {
const headingId = section.title ? `${id}-${s}` : undefined;
return (
<div key={s} data-slot="sidebar-nav-section" className="flex flex-col gap-1">
{section.title && (
<p
id={headingId}
className="m-0 px-2 font-mono text-xs tracking-wide text-muted-foreground uppercase select-none"
>
{section.title}
</p>
)}
<ul aria-labelledby={headingId} className="m-0 flex list-none flex-col gap-px p-0">
{section.items.map((item, i) => (
<li key={`${item.href}-${i}`}>
<Link
href={item.href}
aria-current={item.active ? "page" : undefined}
data-slot="sidebar-nav-item"
className={cn(
"flex h-8 items-center gap-2 rounded-sm px-2 text-sm transition-colors duration-100 ease-out outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 [&_svg]:size-4 [&_svg]:shrink-0",
item.active ? "bg-muted font-medium text-foreground" : "text-muted-foreground",
)}
>
{item.icon && <span className="flex shrink-0 items-center">{item.icon}</span>}
<span className="min-w-0 flex-1 truncate">{item.label}</span>
{item.count !== undefined && (
<span className="font-mono text-xs tabular-nums" aria-label={`${item.count} items`}>
{item.count}
</span>
)}
</Link>
</li>
))}
</ul>
</div>
);
})}
</nav>
);
}
For AI agents
Hand your agent this page, the markdown at /components/sidebar-nav/prompt.md, or the Copy AI prompt button above.
When to use
- The left column of an app or docs site with up to a few dozen links
- When links group into named sections and some carry a count
- Wherever the current page should read as a quiet filled row, not an accent bar
When not to use
- For switching views inside one page, use vertical-tabs or underline-tabs
- For a top-of-page header with a handful of links, use the navbar
- For nested trees that expand and collapse: it is flat sections only
Agent notes
- Server component with next/link, so pass real hrefs and set active yourself from the pathname
- Items are 32px, rounded-sm, text-muted-foreground; hover and active are a bg-muted fill, with aria-current on the active link
- It has no border or padding of its own: wrap it in your sidebar column, or in a rounded-lg border box for demos
- The count takes any number or string and is read as "n items"; icons should be 16px lucide icons