# Navigation

> Router-agnostic links, active-route matching, one NavItem shape and the app shell context that every nav component shares.

- Type: React helper (Hooks & Utilities)
- Access: Free (the source is open to everyone)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add navigation`
- Docs: https://wingo-ui.com/components/navigation

## Interaction

Click through the mini CRM: every link goes through a pretend router given to LinkProvider, the address bar updates and the matching item lights up with aria-current. Open a customer and Customers stays active by prefix; switch match to exact and it lets go. The external link opens a new tab and the disabled one ignores clicks and Tab.

## Installation

```bash
npx wingo-ui@latest add navigation
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/navigation.json`

Files:

- `lib/navigation.tsx`

## Usage

```tsx
"use client"

import type { ReactNode } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { LinkProvider, matchPath, NavLink, useCurrentPath } from "@/lib/navigation"

// app/providers.tsx, rendered by the root layout around {children}
export function Providers({ children }: { children: ReactNode }) {
  return (
    <LinkProvider component={Link} currentPath={usePathname()}>
      {children}
    </LinkProvider>
  )
}

// any link inside renders through next/link and knows when it is the current page
export function ClientsLink() {
  const currentPath = useCurrentPath()
  return (
    <NavLink href="/customers" active={matchPath("/customers", currentPath)} className="data-active:font-semibold">
      Customers
    </NavLink>
  )
}
```

## API

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `currentPath?` | `string` | `""` | LinkProvider({ component, currentPath, onNavigate }): the current pathname (next: usePathname()). Every nav component inside finds its active item from it; a component's own currentPath or value prop wins. |
| `component?` | `ComponentType<LinkComponentProps> \| "a"` | `"a"` | LinkProvider: the router link every NavLink renders through (next/link's Link, react-router's Link). Set it once in a client providers file; library components never import next/*. |
| `onNavigate?` | `(href: string) => void` |  | LinkProvider: runs when a NavLink starts an in-app navigation (a plain left click on an internal link). useNavigateListener subscribes to the same event, which is how a route progress bar starts on its own. |
| `navigate?` | `(href: string) => void` |  | LinkProvider: the router's push (next: useRouter().push). useNavigate() uses it for navigations without an anchor, like a command menu item picked with Enter; without it they fall back to a full page load. |
| `match?` | `"prefix" \| "exact"` | `"prefix"` | matchPath(href, currentPath, match) and NavItem.match: prefix keeps /customers active on /customers/42, exact only on /customers. "/" always matches exactly. |
| `NavLink?` | `({ href, component?, active?, current?, disabled?, external?, ...anchorProps }) => JSX.Element` |  | An anchor through the router link: aria-current (current: "page" \| "step" \| "location") and data-active while active, a real disabled state (no href, aria-disabled, tabIndex -1), and external links (absolute urls to another origin, or external) in a new tab with rel="noopener noreferrer". data-slot="nav-link". |

## Parts

### NavItem (type)

One shape for sidebar, bottom tab bar, nav tabs, nav menu and the command menu, so an app defines its navigation once. navItemKey(item) is id, else href, else a string label. Type: { id?, label, textValue?, href?, icon?, activeIcon?, badge?, description?, disabled?, external?, match?, onSelect?, items?, keywords?, shortcut? }.

### NavGroup (type)

One shape for sidebar, bottom tab bar, nav tabs, nav menu and the command menu, so an app defines its navigation once. navItemKey(item) is id, else href, else a string label. Type: { id?, label?, items, collapsible?, defaultOpen? }.

### findActivePath (function)

The active item and its ancestors ([Settings, Team]): an explicit value (an item key) wins, else the most specific href match. Pass groupItems(groups) for grouped navigation. flattenNavItems, navItemText, isExternalHref and normalizePath are exported too. Signature: (items: NavItem[], { value?, currentPath? }) => NavItem[] | null.

### useLinkComponent (hook)

For component authors: the provider's link component and path with a prop override, a subscription to in-app navigations, and a navigate function (external urls open in a new tab, in-app paths notify listeners and go through the provider's navigate). Signature: (override?) => LinkComponent.

### useCurrentPath (hook)

For component authors: the provider's link component and path with a prop override, a subscription to in-app navigations, and a navigate function (external urls open in a new tab, in-app paths notify listeners and go through the provider's navigate). Signature: (override?) => string | undefined.

### useNavigateListener (hook)

For component authors: the provider's link component and path with a prop override, a subscription to in-app navigations, and a navigate function (external urls open in a new tab, in-app paths notify listeners and go through the provider's navigate). Signature: (listener) => void.

### useNavigate (hook)

For component authors: the provider's link component and path with a prop override, a subscription to in-app navigations, and a navigate function (external urls open in a new tab, in-app paths notify listeners and go through the provider's navigate). Signature: () => (href, { external? }) => void.

### NavShellProvider (component)

What an app shell shares with the bars inside it: isMobile, collapsed and setCollapsed, mobileOpen and setMobileOpen, toggleSidebar, hasSidebar, scroll (window or content) and its labels. A top bar renders the menu button from it and a tab bar knows the shell lays it out, without importing the shell. Props: ({ value: NavShellState, children }).

### useNavShell (hook)

What an app shell shares with the bars inside it: isMobile, collapsed and setCollapsed, mobileOpen and setMobileOpen, toggleSidebar, hasSidebar, scroll (window or content) and its labels. A top bar renders the menu button from it and a tab bar knows the shell lays it out, without importing the shell. Signature: () => NavShellState | null.

## Examples

- **Settings tabs**: Links in a tab bar with exact matching, so General is not active on /settings/billing. Swap DemoLink for next/link in an app.

## See also

- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [useScrollState](https://wingo-ui.com/components/use-scroll-state) ([Markdown](https://wingo-ui.com/components/use-scroll-state.md))

---

Canonical: https://wingo-ui.com/components/navigation
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
