<!-- Protocore Design System — NavLink -->
# NavLink

- **Category:** Navigation (`navigation`)
- **Slug:** `navigation/nav-link`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { NavLink } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/navigation/nav-link

> Navigation link row for sidebars and menus — leading icon, label, optional description and trailing count/chevron, an inked active row with a 2px accent leading rule, and collapsible nested sub-links.

## When to use it

**NavLink** is the row primitive for a **Sidebar**, a slide-in **Sheet** menu, or any vertical navigation list. `Sidebar` gives you the rail, groups, and mono UPPERCASE group labels; `NavLink` is the richer item when you need an icon, a count, a description, or collapsible children.

- Reach for `Sidebar.Item` for a plain label row; reach for `NavLink.Root` when the row carries an icon, count, or description.
- Use `NavLink.Sub` for one level of nesting. Deeper trees read better in a `TreeView`.
- The `count` is metadata (mono, muted) — not a status. For a colored status use a `Badge` in `rightSection`.
- `active` is a visual promise of the current location: set it on exactly one row per view.

## Examples

### Basics

Each row is a `NavLink.Root` with an `icon` and `label`. Mark the current row `active` — it inks and grows the 2px accent leading rule. Add a `count` for an at-a-glance total.

```tsx
import { NavLink } from "@protocore/pds";
import { LayoutDashboard, Network, KeyRound, Webhook, CreditCard } from "lucide-react";

export default function NavLinkBasics() {
  return (
    <nav aria-label="Console" style={{ width: 260 }}>
      <NavLink.Root href="#" label="Overview" icon={<LayoutDashboard />} active />
      <NavLink.Root href="#" label="Networks" icon={<Network />} count={6} />
      <NavLink.Root href="#" label="API keys" icon={<KeyRound />} count={12} />
      <NavLink.Root href="#" label="Webhooks" icon={<Webhook />} />
      <NavLink.Root href="#" label="Billing" icon={<CreditCard />} withChevron />
    </nav>
  );
}
```

### Descriptions

A `description` adds a sans-13 muted second line — useful when a label alone is ambiguous. Keep both lines to one line each; the row truncates rather than wraps.

```tsx
import { NavLink } from "@protocore/pds";
import { Network, Webhook, Activity } from "lucide-react";

export default function NavLinkDescriptions() {
  return (
    <nav aria-label="Resources" style={{ width: 300 }}>
      <NavLink.Root
        href="#"
        label="Networks"
        description="Ethereum, Base, Arbitrum + 3"
        icon={<Network />}
        active
      />
      <NavLink.Root
        href="#"
        label="Webhooks"
        description="3 endpoints, 1 failing"
        icon={<Webhook />}
        count={3}
      />
      <NavLink.Root
        href="#"
        label="Usage"
        description="1.2M requests this cycle"
        icon={<Activity />}
      />
    </nav>
  );
}
```

## Do & don't

**Do**

- Set `active` on exactly one row — the current location — driven by your router.
- Use `NavLink.Sub` for collapsible sections; the caret signals expandability.
- Pass `asChild` to keep client-side navigation with a framework `<Link>`.
- Keep labels short; add a description only when the label is ambiguous.

**Don't**

- Mark more than one row active — the accent is a single-location signal.
- Put a colored status pill in `count`; count is muted metadata.
- Nest `NavLink.Sub` more than one level deep — use TreeView for real trees.
- Rely on the trailing chevron alone to convey state — it is decorative.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Tab` | Move focus to the next row (link or Sub trigger) |
| `Enter` | Follow the focused link, or toggle a Sub disclosure |
| `Space` | Toggle the focused Sub disclosure |

**Notes**

- The active row carries `aria-current="page"` so assistive tech announces the current location.
- NavLink.Sub renders a Radix Collapsible: the trigger is a real `<button>` with `aria-expanded` and its content is associated for screen readers.
- A disabled row sets `aria-disabled` and drops pointer events; give every icon-only affordance a text label via the row's `label`.
- Wrap a set of rows in a `<nav aria-label>` so the group is announced as a navigation landmark.

## Related

`sidebar`, `app-shell`, `menubar`, `breadcrumb`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
