| | |

React Router 06 ⚛️ Active Styling — Styling active NavLink states

⏱️ Estimated read: 10 min | |

The previous chapter covered navigation with the <Link> and <NavLink> components. This chapter focuses specifically on the active state: how the router determines that the current URL matches a link, how the <NavLink> exposes that state to the component, and the different patterns for styling the active link. The LFCS exam does not test React Router, but front-end engineering certification paths do. The critical distinction is between the default active class and the function-based className prop. The default class is active, but it is applied to the <NavLink> without any configuration. The function-based prop receives the isActive and isPending booleans, which allows the component to decide which class to apply and to apply multiple classes or different styles. The end prop is the other critical detail: without it, a <NavLink to="/"> is active on every page, because every path starts with /.

Key point: The <NavLink> applies the active class by default when the current route matches the to prop. The className prop can be a function that receives { isActive, isPending } and returns the class string. The end prop restricts the match to the exact path, which is required for the root link. The style prop can also be a function, which allows inline styles to change based on the active state. The children prop can be a function that receives the same object, which allows the link content to change based on the state.


Why active styling requires a specific mental model

The default class problem. The <NavLink> applies the active class when the current route matches the to prop. If the CSS has no .active rule, the class is applied but has no visible effect. The class name is not configurable through a prop; it is fixed as active. The custom class is the className prop, and the custom logic is the function form.

The partial match problem. The <NavLink to="/users"> is active when the current URL is /users, /users/1, and /users/1/edit. The match is a prefix match, not an exact match. This is the desired behavior for the section navigation: the “Users” link is highlighted when the user is anywhere in the users section. The <NavLink to="/"> is active on every page for the same reason, which is why the end prop is required for the root link.

The isPending problem. The isActive is true when the current route matches the to prop. The isPending is true during the navigation transition, which is the time between the click and the new route’s loader completing. The isPending is only relevant in the Data mode, where the loaders run. The function form of the className receives both booleans, which allows the component to style the pending state differently from the active state.

The aria-current problem. The <NavLink> sets the aria-current="page" attribute on the active link. This is the accessibility feature: the screen reader announces the current page. The attribute is set automatically, and the CSS can target it with [aria-current="page"]. The aria-current is the standard, and the active class is the convenience.

The trade-off. The default active class is the simplest. The function-based className is more flexible. The style function is the correct choice for the inline styles. The children function is the choice for the content that changes based on the state, such as a badge or an icon.


a. The default active class

The <NavLink> applies the active class when the route matches. The CSS targets the class.

import { NavLink } from "react-router";

function NavBar() {
  return (
    <nav>
      <NavLink to="/">Home</NavLink>
      <NavLink to="/about">About</NavLink>
      <NavLink to="/users">Users</NavLink>
    </nav>
  );
}

The rendered HTML when the current route is /about is:

<a href="/about" class="active" aria-current="page">About</a>

The CSS applies the style:

a.active {
  font-weight: bold;
  color: #1a73e8;
}

a[aria-current="page"] {
  border-bottom: 2px solid #1a73e8;
}

The active class and the aria-current attribute are both applied. The aria-current is the accessibility hook. The active is the styling hook.


b. The function-based className and style

The className prop can be a function that receives the isActive and isPending booleans.

<NavLink
  to="/about"
  className={({ isActive, isPending }) =>
    isActive ? "nav-active" : isPending ? "nav-pending" : "nav"
  }
>
  About
</NavLink>

The function returns a string. The string is the class applied to the <a>. The isActive is true when the route matches. The isPending is true during the navigation transition.

The style prop can also be a function.

<NavLink
  to="/about"
  style={({ isActive }) => ({
    fontWeight: isActive ? "bold" : "normal",
    color: isActive ? "#1a73e8" : "#333",
  })}
>
  About
</NavLink>

The function returns the style object. The object is merged with the other styles on the element.

The children prop can be a function, which allows the link content to change based on the state.

<NavLink to="/messages">
  {({ isActive }) => (
    <span>
      Messages {isActive && <span className="badge">new</span>}
    </span>
  )}
</NavLink>

The function returns the content. The isActive is true when the route matches. The content can conditionally render an icon, a badge, or a different label.


c. The end prop and the exact match

The end prop restricts the match to the exact path. The <NavLink to="/" end> is active only when the current URL is /, not when it is /about or /users/1.

<NavLink to="/" end>Home</NavLink>

The rendered HTML when the current route is / is:

<a href="/" class="active" aria-current="page">Home</a>

The rendered HTML when the current route is /about is:

<a href="/">Home</a>

Without the end prop, the / link is active on every page. The end prop is the fix.

The end prop is also useful for the parent routes. The <NavLink to="/users" end> is active only when the URL is /users, not when it is /users/1. The default behavior is the prefix match, which is the desired behavior for the section navigation. The end prop is the exception for the root link and for the parent route that should not be highlighted when a child is active.


Complete Example Session

# ============================================
# PART 1: BASIC NAVLINK
# ============================================

<NavLink to="/about">About</NavLink>
# ============================================
# PART 2: DEFAULT ACTIVE CLASS
# ============================================

a.active { font-weight: bold; }
# ============================================
# PART 3: FUNCTION CLASSNAME
# ============================================

<NavLink
  to="/about"
  className={({ isActive }) => isActive ? "nav-active" : "nav"}
>
  About
</NavLink>
# ============================================
# PART 4: FUNCTION STYLE
# ============================================

<NavLink
  to="/about"
  style={({ isActive }) => ({ fontWeight: isActive ? "bold" : "normal" })}
>
  About
</NavLink>
# ============================================
# PART 5: FUNCTION CHILDREN
# ============================================

<NavLink to="/messages">
  {({ isActive }) => <span>Messages {isActive && "•"}</span>}
</NavLink>
# ============================================
# PART 6: ROOT LINK WITH END
# ============================================

<NavLink to="/" end>Home</NavLink>
# ============================================
# PART 7: PARTIAL MATCH
# ============================================

<NavLink to="/users">Users</NavLink>
// Active on /users, /users/1, /users/1/edit
# ============================================
# PART 8: ARIA CURRENT
# ============================================

a[aria-current="page"] { border-bottom: 2px solid #1a73e8; }
# ============================================
# PART 9: PENDING STATE
# ============================================

<NavLink
  to="/about"
  className={({ isActive, isPending }) =>
    isPending ? "pending" : isActive ? "active" : ""
  }
>
  About
</NavLink>

The nine parts trace the active styling: basic NavLink, default class, function className, function style, function children, end prop, partial match, aria-current, and pending state.


Quick Reference

NavLink Props

PropTypePurpose
tostring or objectDestination
endbooleanExact match
classNamestring or functionCSS class
styleobject or functionInline style
childrennode or functionLink content

Function Arguments

ArgumentTypeMeaning
isActivebooleanRoute matches
isPendingbooleanNavigation in progress

Default Attributes

AttributeValue
classactive (when matched)
aria-current"page" (when matched)

Common Patterns

PatternPurpose
to="/" endRoot link
to="/users"Section prefix match
className={({isActive}) => ...}Custom class
style={({isActive}) => ...}Custom inline style

Best Practices

✅ Do This:

<NavLink to="/" end>Home</NavLink>
<!-- Use end for the root link -->                   # ✅
<NavLink className={({ isActive }) => isActive ? "active" : ""}>About</NavLink>
<!-- Use the function form for custom classes -->    # ✅
a[aria-current="page"] { font-weight: bold; }
<!-- Target the accessibility attribute -->          # ✅

❌ Don’t Do This:

<NavLink to="/">Home</NavLink>
<!-- Missing end; active on every page -->           # ❌
# Style .active without checking the aria-current
<!-- The class is a convenience; aria is the standard --> # ❌
# Use the active state for the page content
<!-- The active state is for the navigation only -->  # ❌

Common Pitfalls

PitfallWhy It HappensFix
Root link always activeNo end propAdd end
Active class not styledNo CSS for .activeAdd the CSS
Function not appliedWrong prop typeUse function form
Pending state not styledIgnoring isPendingUse the function argument
Screen reader not announcingNot targeting aria-currentThe attribute is automatic; use it in CSS

Real-World Examples

1. Default Class

<NavLink to="/about">About</NavLink>

2. Function Class

<NavLink className={({ isActive }) => isActive ? "active" : ""}>About</NavLink>

3. Function Style

<NavLink style={({ isActive }) => ({ fontWeight: isActive ? "bold" : "normal" })}>About</NavLink>

4. Function Children

<NavLink to="/messages">{({ isActive }) => <span>{isActive ? "Active" : "Messages"}</span>}</NavLink>

5. Root with End

<NavLink to="/" end>Home</NavLink>

6. Parent with End

<NavLink to="/users" end>Users</NavLink>

7. Pending State

<NavLink className={({ isPending }) => isPending ? "loading" : ""}>About</NavLink>

8. Active CSS

a.active { font-weight: bold; }

9. Aria CSS

a[aria-current="page"] { border-bottom: 2px solid; }

10. Combined

<NavLink
  to="/about"
  className={({ isActive }) => isActive ? "active" : ""}
  style={({ isActive }) => ({ color: isActive ? "blue" : "black" })}
>About</NavLink>

Visual

Match Modes

┌──────────────────────────────────────────────┐
│  <NavLink to="/users">                       │
│  Current URL: /users/1/edit                  │
│  → active (prefix match)                     │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  <NavLink to="/users" end>                   │
│  Current URL: /users/1/edit                  │
│  → inactive (exact match required)           │
└──────────────────────────────────────────────┘

Function Props

┌──────────────────────────────────────────────┐
│  className={({ isActive, isPending }) => ...}│
│  style={({ isActive }) => ...}               │
│  children={({ isActive }) => ...}            │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  isActive: route matches                     │
│  isPending: navigation in progress           │
└──────────────────────────────────────────────┘

Summary

ItemValue
Active classactive (default)
Accessibilityaria-current="page"
Exact matchend prop
Custom classclassName function
Custom stylestyle function
Custom contentchildren function
Function args{ isActive, isPending }
Prefix matchDefault behavior

Key takeaways:

  • The active class is the default. The <NavLink> applies it when the route matches. The aria-current="page" attribute is also applied, which is the accessibility standard. The CSS can target either one.
  • The function-based className receives isActive and isPending. The function returns the class string. The isActive is true when the route matches. The isPending is true during the navigation transition.
  • The end prop is required for the root link. Without it, the <NavLink to="/"> is active on every page. The end restricts the match to the exact path.
  • The style and children props can also be functions. The style function returns the inline style object. The children function returns the link content. Both receive the same { isActive, isPending } object.
  • The prefix match is the default for the section navigation. The <NavLink to="/users"> is active on /users, /users/1, and /users/1/edit. This is the desired behavior for the section links.

Remember: The active state is the navigation highlight. The active class is the default. The aria-current is the accessibility. The end is the exact match. The function props are the customization. The exam tests the end prop and the function forms.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!