Skip to content
goodtabs UI
BrowseBrowse components

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

PropTypeDefaultDescription
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.
widthstringCSS 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