skip to content

In React Router v7, how does <NavLink> decide that it is active, and what does its end prop change?

level: juniorimportance: must knowfreq 66%

answer

  1. prefix match on the pathname
  2. segment boundary after the prefix
  3. end means exact match only
  4. active class and aria-current

basics

~10 s

<NavLink> is active when the current pathname equals its resolved target or continues past it at a slash, so /settings is active on /settings/profile. The end prop limits that to an exact match.

solid answer

~40 s

`<NavLink>` resolves its `to` and compares it with the current `location.pathname`, case-insensitively by default. It is active when the paths are equal, or when the current path starts with the target and the next character is `/`, so `/settings` lights up on `/settings/profile` but `/set` does not. `end` removes the prefix case, so `<NavLink to="/tasks" end>` is active only on `/tasks`. `to="/"` is special-cased and only matches the root. When active, it adds the `active` class (if `className` is a string) and `aria-current="page"`; `className`, `style` and `children` can be functions of `{ isActive, isPending, isTransitioning }`. `isPending` is only populated in data and framework modes.

code

tsx · 21 lines
tsx
import { NavLink } from "react-router";

const item = ({ isActive, isPending }: { isActive: boolean; isPending: boolean }) =>
  isPending ? "nav-item is-loading" : isActive ? "nav-item is-current" : "nav-item";

export function SettingsSidebar() {
  return (
    <nav aria-label="Settings">
      {/* exact: must not stay lit on /settings/profile */}
      <NavLink to="/settings" end className={item}>
        Overview
      </NavLink>
      <NavLink to="/settings/profile" className={item}>
        Profile
      </NavLink>
      <NavLink to="/settings/billing" className={item}>
        Billing
      </NavLink>
    </nav>
  );
}

go deeper

for a junior

Recall that NavLink is a Link with an active state, gets the active class and aria-current, and that end stops parent links matching child pages.

for a middle

Explain the segment-boundary prefix rule, the root special case, case-insensitivity, and why a className function suppresses the default active class.

for a senior

Show judgment in menu design: which links need end, why pathname-only matching fails search-param tabs, and when pending styling needs a data router.

for a principal

Frame active-state logic as part of a navigation design system: consistent end usage and aria-current semantics across teams avoid double-highlighted menus.

## What `<NavLink>` adds to `<Link>` `<NavLink>` is a `<Link>` that knows whether it points at the page the user is on. It renders the same anchor and handles clicks the same way, but it also computes an **active state** (and, with a data router, a **pending state**) and exposes them for styling and accessibility. It is the component for navigation menus, tab bars and sidebars. ## The matching rule On every render, `<NavLink>` resolves its `to` into a pathname (relative `to` values resolve against the route it is rendered in) and compares it with `location.pathname`: 1. Both paths are lower-cased first unless you pass `caseSensitive`. 2. If they are **equal**, the link is active. 3. Otherwise, unless `end` is set, the link is active when the current path **starts with** the target **and the next character is `/`**. The slash check is what makes this a segment match rather than a string prefix match: | NavLink | Current URL | Active? | |---|---|---| | `to="/settings"` | `/settings` | yes | | `to="/settings"` | `/settings/profile` | yes | | `to="/settings" end` | `/settings/profile` | no | | `to="/set"` | `/settings` | no (no slash after the prefix) | | `to="/"` | `/settings` | no (root is special-cased) | Only the **pathname** takes part. The query string and hash are ignored, so a NavLink to `/products` is active on `/products?page=2` too. ## The root link special case Every URL begins with `/`, so a naive prefix rule would keep a Home link permanently active. React Router treats `<NavLink to="/">` as an exception: it only matches at the root, effectively ignoring `end`. You will still see `end` on Home links in examples; it is harmless there. ## Where `end` matters `end` is for a parent-section link whose child pages should *not* highlight it. A typical settings sidebar has an "Overview" item pointing at `/settings` and items for `/settings/profile` and `/settings/billing`. Without `end`, Overview stays highlighted on every child page, so two items look selected at once. Adding `end` to Overview fixes that, while the section link in the top navigation (also `/settings`) should stay **without** `end` so it remains lit throughout the section. ## Styling and accessibility output - **Default class**: when `className` is a string (or omitted), NavLink appends `active` when active, `pending` when pending and `transitioning` during a view transition. - **Function props**: `className`, `style` and `children` may be functions receiving `{ isActive, isPending, isTransitioning }`. If `className` is a function, the automatic `active` class is **not** added; your function decides the whole class list. - **`aria-current`**: an active NavLink gets `aria-current="page"` (overridable through the `aria-current` prop), so screen readers announce the current page. ## Pending state and modes `isPending` is true while a navigation is in progress **to** this link's location. That information only exists in a data router (`createBrowserRouter` with `RouterProvider`) or framework mode, which track the in-flight navigation. With `<BrowserRouter>` (declarative mode) there is no pending navigation to observe, so `isPending` stays false and only `isActive` is useful. ## Mistakes interviewers listen for - **Double-highlighted menus.** An overview item and its child items all lit at once is the classic sign of a missing `end` on the overview link. - **Building a tab bar on NavLink when the tabs are search params.** Because matching ignores the query string, every tab reads as active; derive the selected tab from the query instead. - **Styling with `className={isActive ? ...}` outside the function form.** `isActive` is not in scope in the parent; it only exists as an argument of the function props. - **Dropping `aria-current`.** Overriding the markup through `children` is fine, but the anchor must still carry `aria-current="page"` when active, which NavLink adds for you unless you override the prop. - **Using NavLink everywhere.** A link that never needs an active style is simpler and clearer as `<Link>`. In an interview, a strong answer names the rule (equal, or prefix plus slash), the one exception (the root), the one switch (`end`) and the one mode dependency (`isPending` needs a data router). ## Summary - Active means equal, or a prefix followed by `/`; comparisons ignore case unless `caseSensitive`. - `end` requires an exact match; the root link is exact by default. - Output is the `active` class and `aria-current="page"`, or whatever your function props return. - `isPending` needs a data or framework router.

  • In React Router v7, why might a NavLink to /products stay highlighted when the user filters with ?category=shoes?
    NavLink compares pathnames only, so the query string never affects `isActive`. That is usually what you want for a section link. If a tab bar is driven by a search param, derive the active tab from the search params yourself rather than expecting NavLink to distinguish `?tab=a` from `?tab=b`.
  • In React Router v7, how would you make a NavLink's active state distinguish /Docs from /docs?
    Pass `caseSensitive`. By default NavLink lower-cases both the target and the current pathname before comparing, so case differences never change the active state. With `caseSensitive`, the comparison uses the paths as written.

saying these in an interview costs you the question

  • NavLink uses a plain string prefix, so to="/set" is active on /settings
  • The end prop makes NavLink matching case-sensitive
  • A className function still gets the automatic active class added
  • isPending works the same under BrowserRouter as under a data router
  • NavLink treats different query strings as different active states