<Link>

Navigates between routes on the client, prefetching the target route before the user clicks.

Signature
import Link from 'next/link'

<Link
  href={string | UrlObject}          // required
  replace={boolean}                  // default: false
  scroll={boolean}                   // default: true
  prefetch={boolean | 'auto' | null} // default: null ("auto")
  onNavigate={(event: { preventDefault(): void }) => void}
  transitionTypes={string[]}
  {...anchorAttributes}
>
  {children}
</Link>
  • Added in Next.js v1.0.0

Parameters

ParameterTypeDescription
$hrefstring | UrlObjectThe path or URL to navigate to. An object form such as { pathname: '/about', query: { name: 'test' } } is serialised into a query string.
$replaceoptionalboolean= falseReplaces the current entry in the browser history stack instead of pushing a new one.
$scrolloptionalboolean= trueWhen false, Next.js will not scroll to the first Page element after navigation. When true it keeps the scroll position if the new Page is visible, and scrolls to the top of it if it is not.
$prefetchoptionalboolean | "auto" | null= null"auto" or null prefetches the full route for static routes and the partial route down to the nearest loading.js boundary for dynamic routes; true prefetches the full route in both cases; false disables prefetching on viewport entry and on hover.
$onNavigateoptional(event: { preventDefault(): void }) => voidRuns during client-side navigation only. Calling event.preventDefault() cancels the navigation.
$transitionTypesoptionalstring[]Transition types passed to React.addTransitionType inside the navigation transition, so <ViewTransition> components can animate per navigation kind.

Return value

JSX.ElementAn <a> element with the same href, wired to the App Router for client-side navigation and prefetching.

Errors

Next.js raises an invalid-prop error when href is missing or is neither a string nor a URL object.

Changelog

  • PHP v16.2.0transitionTypes prop added.
  • PHP v15.4.0"auto" added as an alias for the default prefetch behaviour.
  • PHP v15.3.0onNavigate prop added, giving a cancellable hook into client-side navigation.
  • PHP v13.0.0A child <a> tag is no longer required - <Link> renders the anchor itself. Nesting one now duplicates the element.
  • PHP v10.0.0href values pointing at a dynamic route resolve automatically; the as prop is no longer required.

<Link> renders a real <a> element and layers prefetching and client-side transitions on top of it. Reach for it for every in-app route change. Keep a bare <a> only for external URLs, downloads, or a route where you deliberately want a full document load. Anchor attributes such as className, target and aria-current pass straight through to the underlying element.

What prefetching actually does

In production, a link that scrolls into the viewport triggers a background fetch of the target route. Static routes are fetched whole; dynamic routes are fetched only down to the nearest loading.js boundary. That is why a dynamic route with no loading.js can still feel slow after a prefetch — there was nothing cheap to prefetch. Prefetching is disabled entirely in next dev.

What trips people up

  • Navigation does not jump to the top by default. It preserves scroll position while the new Page is visible in the viewport, and only scrolls when it is not.
  • onNavigate is not onClick. Cmd/Ctrl-clicks, external URLs and download links fire onClick but never onNavigate.
  • Next.js skips sticky and fixed elements when picking the scroll target, so content can land under a sticky header. Fix it with scroll-padding-top on html.

Example

Example · typescript
'use client'

import Link from 'next/link'
import { usePathname } from 'next/navigation'

const routes = [
  { href: '/', label: 'Home' },
  { href: '/dashboard', label: 'Dashboard' },
  { href: '/settings', label: 'Settings' },
]

export function NavLinks() {
  const pathname = usePathname()

  return (
    <nav>
      {routes.map((route) => (
        <Link
          key={route.href}
          href={route.href}
          aria-current={pathname === route.href ? 'page' : undefined}
        >
          {route.label}
        </Link>
      ))}
    </nav>
  )
}

When to use it

  • Build a site navigation bar that highlights the current route with usePathname().
  • Render a list of blog post links from a dynamic segment without a full page reload.
  • Warn a user with unsaved form changes before letting a navigation proceed.
  • Update a URL without adding a history entry by passing replace.

Frequently asked questions

Why is prefetching not working in development?

Prefetching is only enabled in production builds. Run next build && next start to observe it; next dev deliberately skips it so you are not rebuilding routes you never visit.

Do I still need an <a> tag inside <Link>?

No - since v13.0.0 <Link> renders the <a> itself, and nesting one produces a duplicate anchor. Put className, target and other anchor attributes directly on <Link>.

Why does the page not scroll to the top after navigation?

That is the documented default: <Link> preserves scroll position as long as the new Page element is visible in the viewport, mirroring browser back/forward behaviour. It only scrolls when the Page is out of view.

More examples

Linking to dynamic segments from a Server Component

A long list is exactly where the default prefetch behaviour hurts: every row that scrolls into view would fire a request. prefetch={false} turns off both viewport and hover prefetching, so the fifty links on the page cost nothing until one is clicked. Note that this file has no 'use client' directive - <Link> renders fine from a Server Component.

Example · typescript
import Link from 'next/link'
import { getPosts } from '@/lib/posts'

export default async function BlogIndex() {
  const posts = await getPosts()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`} prefetch={false}>
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  )
}

Cancelling a navigation with onNavigate

event.preventDefault() inside onNavigate stops the client-side transition and leaves the URL untouched. Because onNavigate only runs for same-origin client navigations, a Cmd-click that opens a new tab bypasses the guard entirely - the browser handles that case, not the router.

Example · typescript
'use client'

import Link from 'next/link'
import type { ComponentProps } from 'react'

type GuardedLinkProps = ComponentProps<typeof Link> & { dirty: boolean }

export function GuardedLink({ dirty, children, ...props }: GuardedLinkProps) {
  return (
    <Link
      {...props}
      onNavigate={(event) => {
        if (dirty && !window.confirm('You have unsaved changes. Leave anyway?')) {
          event.preventDefault()
        }
      }}
    >
      {children}
    </Link>
  )
}

Object href and disabling the scroll jump

The object form of href builds the query string for you and avoids hand-encoding values. The second link shows the usual reason to pass scroll={false}: the hash fragment already places the viewport, and letting the router also scroll to the Page element would undo it.

Example · typescript
import Link from 'next/link'

export default function Page() {
  return (
    <>
      {/* Navigates to /about?name=test */}
      <Link href={{ pathname: '/about', query: { name: 'test' } }}>About</Link>

      {/* Jumps to an id without Next.js also scrolling the Page element */}
      <Link href="/dashboard#settings" scroll={false}>
        Settings
      </Link>

      {/* Overwrites the current history entry */}
      <Link href="/login" replace>
        Sign in
      </Link>
    </>
  )
}

Discussion

  • Be the first to comment on this lesson.