Skip to main content

Design system components

NavLink

Navigation anchor primitive for site chrome such as SiteHeader, drawers and in-page navs. Renders an underlying next/link; on the exact current page it renders a non-focusable <span aria-current='page'> instead of a link, and a section link that is an ancestor of the current route gets aria-current='true'.

Status: stable. Group: navigation. Markdown version: /design-system/components/nav-link.md

When to use

NavLink is the router-aware navigation link for site chrome: headers, drawers and sidebars. It compares the current pathname against href (prefix match by default, exact with exact) and reflects the result both as aria-current and as the active/inactive class pair. When pathname equals href exactly, the item renders as a non-interactive <span aria-current="page"> instead of a link; an ancestor section link keeps its link and gets aria-current="true". Use it inside a labelled <nav>; for inline prose links use Link instead. Styling is deliberately className-driven (Tailwind tokens by default) so each navigation surface owns its look while the active-state contract stays shared.

When not to use

  • Do not use inside body prose; that is Link.
  • Do not use as a button; primary actions belong on Button.
  • Do not use NavLink for inline body links; that is Link's job; NavLink's route tracking is navigation-chrome behavior.

Look-alikes

  • BlogNav: Do not use BlogNav as site-wide navigation; use SiteHeader or NavLink instead.
  • CategoryFilter: Do not use as primary navigation; CategoryFilter is a UI control, not a nav surface; use NavLink for routes.
  • Link: Do not use NavLink for inline body links; that is Link's job; NavLink's route tracking is navigation-chrome behavior.
  • Tabs: Do not use tabs for page navigation; that is NavLink in a nav landmark; tabs switch views inside one page.

Props

NavLink props
PropTypeDefaultDescriptionRequired
hrefstringnoneRoute path for next/link.Yes
childrenReactNodenoneNavigation link label.Yes
exactbooleanfalseMatch href exactly for the active state instead of by prefix.No
classNamestringnoneBase class names applied in every state.No
activeClassNamestringtext-foregroundClass names applied when the route is active.No
inactiveClassNamestringtext-muted-foreground hover:text-foregroundClass names applied when the route is inactive.No
onClick() => voidnoneCalled when the link is activated. Fires only for the navigable anchor; the current-page indicator is a non-interactive span and never calls it. Lets containers (the mobile drawer) close as soon as the visitor taps a destination instead of waiting for the route change to land.No

Variants

No variant dimensions and no props with a fixed set of values.

Invalid combinations

The contract records no invalid combinations.

Keyboard

NavLink keyboard behaviour
KeyBehaviour
TabFocuses each link. The exact current-page item is a <span> with no href or tabindex, so it is skipped.
EnterFollows the link.

Rules

Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.

NavLink rules
LevelRuleHow it is checked
MUSTARIA: Marks the active item with aria-current so it is announced. The exact current page is a <span aria-current='page'>; with the default prefix match, a section link whose href is a parent of the current path (href /work on /work/foo) stays a link with aria-current='true', because it is the current section, not the current page.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Renders a real <a> (via next/link) for native browser behaviour, except on the exact current page: there it renders a <span aria-current='page'> with no href, so the item leaves the Tab order and screen-reader link lists.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTRequired stories produce no axe violations at the configured severity.Automated by npm run test:stories:matrix:ci. WCAG 2.5.8.
MUSTThe accessibility tree (role, name, state) matches the committed snapshot for every required story.Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2.
MUSTThe component remains legible and operable under a real browser forced-colors: active pass.Automated by npm run test:stories:hc.
MUSTEvery documented keyboard interaction works as specified.Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2.
MUSTA human completed an end-to-end accessibility review of this component.Manual review, last on 2026-11-04.
SHOULDWrap groups in a labelled <nav aria-label="Primary"> so assistive tech can jump to the landmark.Not stated in the contract.
SHOULDUse exact on short roots like "/"; prefix matching would otherwise mark Home active on every route.Not stated in the contract.
SHOULDKeep active styling redundant with aria-current: weight or underline plus color, never color alone.Not stated in the contract.
SHOULDOverride className/activeClassName per surface instead of wrapping; the active-state logic is the shared part.Not stated in the contract.
SHOULD NOTDo not hand-set aria-current on children; the component derives it from the route.Not stated in the contract.

Install and import

  • Install: npm install @digitaltableteur/react (version 0.9.1, restricted access on npm).
  • Import: import { NavLink } from "@digitaltableteur/react";
  • Entry point: @digitaltableteur/react/navigation exports it too.
  • Styles: import @digitaltableteur/react/style.css once at the app boundary.
  • Also available as the custom element <dt-nav-link>; see the custom elements manifest.