useLinkStatus()

Track the pending state of a parent Link component for inline navigation feedback.

Signature
import { useLinkStatus } from 'next/link'

const { pending } = useLinkStatus()
  • Added in Next.js v15.3.0

Return value

{ pending: boolean }An object with a single property, pending: true from the click until the browser history updates, false after.

Errors

Not supported in the Pages Router, where it always returns { pending: false } instead of erroring.

Changelog

  • PHP v15.3.0useLinkStatus introduced.

useLinkStatus reports whether the navigation started by an enclosing <Link> is still in flight. It exists for one narrow job: giving the user inline confirmation that their click registered when the transition is not instant. The component calling it must be rendered inside the <Link> subtree — that is how it finds the link's state.

When it actually fires

Most of the time it will not. If the destination was prefetched, the pending phase is skipped entirely and you see nothing, which is why the hook seems broken in production on static routes. It earns its place when prefetch={false} is set, when prefetching has not finished, or when the destination is dynamic and has no loading.js. Before adding it, consider whether a loading.js fallback on the destination route is the better fix — that gives an instant transition instead of a spinner on the link.

What trips people up

  • Called outside a <Link> descendant it never reports pending.
  • Click several links quickly and only the last one shows a pending state.
  • Rendering the indicator conditionally causes layout shift. Always render a fixed-size element and toggle opacity or an animation instead.
  • Fast navigations produce a flash. Delay the animation ~100ms so it only appears when the wait is real.

Example

Example · typescript
'use client'

import { useLinkStatus } from 'next/link'

export default function LoadingIndicator() {
  const { pending } = useLinkStatus()

  // Always rendered at a fixed size: only the class changes,
  // so turning the hint on never shifts the layout.
  return (
    <span aria-hidden className={`link-hint ${pending ? 'is-pending' : ''}`} />
  )
}

When to use it

  • Show a small pulsing dot next to a nav link whose route has prefetch={false}.
  • Fade a menu item while a dynamic route without loading.js is being fetched.
  • Give inline feedback on a slow link inside a table row where a full-page fallback would be too heavy.
  • Confirm a click on a link that leads to a route behind an authorization check.

Frequently asked questions

Why is pending always false in useLinkStatus?

The destination was almost certainly prefetched, in which case the pending phase is skipped. Set prefetch={false} on the Link to see it, and check the hook is called from a component rendered inside the Link, not from the component that renders it.

How do I show a loading indicator during navigation in Next.js?

For a route-level fallback add a loading.js file to the destination segment; for inline feedback on the link itself use useLinkStatus in a child of that Link.

Does useLinkStatus work with the Pages Router?

No. It is App Router only and always returns { pending: false } in the Pages Router.

More examples

Rendering the indicator inside each Link

Each LoadingIndicator sits inside its own Link, so each one reports only that link's navigation. prefetch={false} is what makes the pending state observable here; with the default prefetching, the transition usually completes before any indicator could appear.

Example · typescript
// app/shop/layout.tsx
import Link from 'next/link'
import LoadingIndicator from '../components/loading-indicator'

const links = [
  { href: '/shop/electronics', label: 'Electronics' },
  { href: '/shop/clothing', label: 'Clothing' },
  { href: '/shop/books', label: 'Books' },
]

function Menubar() {
  return (
    <div>
      {links.map((link) => (
        <Link key={link.label} href={link.href} prefetch={false}>
          <span className="label">{link.label}</span> <LoadingIndicator />
        </Link>
      ))}
    </div>
  )
}

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <Menubar />
      {children}
    </div>
  )
}

Delaying the hint so fast navigations stay quiet

The element occupies its space at all times, so toggling it never reflows the link. The 100ms animation delay means a navigation that resolves quickly finishes before anything becomes visible, which removes the flicker users read as a glitch.

Example · css
/* app/styles/global.css */
.link-hint {
  display: inline-block;
  width: 0.6em;
  height: 0.6em;
  margin-left: 0.25rem;
  border-radius: 9999px;
  background: currentColor;
  opacity: 0;
  visibility: hidden; /* reserve space without showing the hint */
}

.link-hint.is-pending {
  visibility: visible;
  animation-name: fadeIn, pulse;
  animation-duration: 200ms, 1s;
  animation-delay: 100ms, 100ms; /* only appears if the wait is real */
  animation-timing-function: ease, ease-in-out;
  animation-iteration-count: 1, infinite;
  animation-fill-mode: forwards, none;
}

@keyframes fadeIn {
  to {
    opacity: 0.35;
  }
}

@keyframes pulse {
  50% {
    opacity: 0.15;
  }
}

Dimming the link label while pending

PendingLabel must be a separate component: the hook reads the Link's context, so calling it in the component that renders the Link itself would be outside the subtree. Opacity changes nothing about the box, so the row height stays fixed.

Example · typescript
'use client'

import Link from 'next/link'
import { useLinkStatus } from 'next/link'

function PendingLabel({ children }: { children: React.ReactNode }) {
  const { pending } = useLinkStatus()

  return (
    <span style={{ opacity: pending ? 0.5 : 1, transition: 'opacity 150ms' }}>
      {children}
    </span>
  )
}

export function ReportLink({ id }: { id: string }) {
  return (
    <Link href={`/reports/${id}`} prefetch={false}>
      <PendingLabel>Open report {id}</PendingLabel>
    </Link>
  )
}

Discussion

  • Be the first to comment on this lesson.