Side nav
Section navigation as wide as its longest label, with aria-current for the page.
Orders
32 orders this week · 4 to ship today
Installation
$ pnpm dlx shadcn@latest add @ssntl/side-navUsage
import { SideNav, SideNavItem } from "@/components/ui/side-nav"
<div className="grid grid-cols-[auto_minmax(0,1fr)] gap-6">
<SideNav aria-label="Store">
<SideNavItem href="/orders" current><Package /> Orders</SideNavItem>
<SideNavItem href="/customers"><Users /> Customers</SideNavItem>
</SideNav>
<main>…</main>
</div>- Content-sized: the nav is as wide as its longest label (an auto column); the content takes the rest.
- current sets aria-current="page"; the active item also changes weight and fill, not color alone.
- Items are 44px tall with the label and an optional 20px icon; keep labels to one or two words.
Code
import * as React from "react"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
// A vertical list of sections. It is as wide as its longest label, so the
// content beside it gets the room; never half the screen for three words.
// The current page is marked with aria-current, not color alone.
function SideNav({
className,
children,
...props
}: React.ComponentProps<"nav"> & { "aria-label": string }) {
return (
<nav
data-slot="side-nav"
className={cn("w-max max-w-full", className)}
{...props}
>
<ul className="grid gap-1">{children}</ul>
</nav>
)
}
function SideNavItem({
asChild = false,
current = false,
className,
...props
}: React.ComponentProps<"a"> & {
/** Render the child element (a framework Link, for example). */
asChild?: boolean
/** The page being shown: sets aria-current="page". */
current?: boolean
}) {
const Comp = asChild ? Slot.Root : "a"
return (
<li>
<Comp
data-slot="side-nav-item"
aria-current={current ? "page" : undefined}
className={cn(
"flex min-h-11 items-center gap-3 rounded-full px-4 text-label font-normal whitespace-nowrap text-muted-foreground",
"transition-[background-color,color] duration-(--duration-fast) ease-out hover:bg-secondary hover:text-foreground",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
"aria-[current=page]:bg-secondary aria-[current=page]:font-medium aria-[current=page]:text-foreground",
"[&_svg]:size-5 [&_svg]:shrink-0",
className
)}
{...props}
/>
</li>
)
}
export { SideNav, SideNavItem }