Relative Links in Next.js: A Shortcut for Nested Routes

2 min read

How to use relative links in Next.js <Link> component, with an example showing simpler navigation.

When working on a Next.js project with deeply nested routes, I wanted a simple way to navigate “up” the hierarchy without composing the entire path every time.

Normally this wouldn’t be much of a problem, but in my case, I had dynamic segments in the URL (like /tasks/[id]/details/...), which meant that I had to

  • await{:js} the id param to every page that needed to display links
  • pass down the id to every server component that needed to build links

Also not a huge deal, but it felt unnecessary, so I searched for alternatives.

At first, I thought this wasn’t possible since the Next.js docs don’t explicitly cover it. But then I tried:

      <Link href="..">Back</Link>
    

And it worked. It navigated me up two levels, but why?

#Standard HTML Behaviour

Then I realized why there was no documentation for this behaviour: It’s actually just how anchor tags work in HTML and since the <Link>{:tsx} is a wrapper around an <a>{:html} tag, the underlying behavior is the same.

To demonstrate this, imagine you’re on this URL:

      https://yourdomain.com/customer/1/details/address
    

Here’s how different relative links behave:

app / customer / [id] / details / address / page.tsx
      import Link from "next/link";

export default async function Page() {
  return (
    <div>
      {/* Same folder, sibling site → /customer/1/details/billing */}
      <Link href="billing">Billing</Link>

      {/* Same folder, parent site → /customer/1/details */}
      <Link href=".">Customer Details</Link>

      {/* One level up → /customer/1 */}
      <Link href="..">Customer</Link>

      {/* Two levels up → /customer */}
      <Link href="../..">Customer list</Link>
    </div>
  );
}
    

A little note from me

I put a lot of effort into creating content that is informative, engaging, and useful for my readers. If you find it valuable, please consider subscribing to my newsletter.

#Comparison in practice

Here’s a quick comparison from my project.

Before (absolute paths):

      export default async function AddressPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  return (
    <div>
      <h1>Product Address</h1>
      {/* ... */}
      <Link href={`/customer/${id}/details`}>Back to Details</Link>
      <Link href={`/customer/${id}`}>Back to Product</Link>
      <Link href="/customer">All customers</Link>
    </div>
  );
}
    

After (relative paths):

      export default function AddressPage() {
  return (
    <div>
      <h1>Product Address</h1>
      {/* ... */}
      <Link href=".">Back to Details</Link>
      <Link href="..">Back to Product</Link>
      <Link href="/customer">All customers</Link>
    </div>
  );
}
    

What I like is that the links still work, even when the dynamic segment name changes.