Skip to main content
Back to Blog
Tutorials
3 min read
November 16, 2024

How to Design a Custom 404 Page in Next.js

Build a custom 404 page that keeps visitors engaged. Search suggestions, popular links, and design tips for error pages.

Ryel Banfield

Founder & Lead Developer

A generic 404 page loses visitors. A well-designed one redirects them to useful content and reinforces your brand. Here is how to build one.

Step 1: Create the Not Found Page

In the App Router, create not-found.tsx at the app root:

// app/not-found.tsx
import Link from "next/link";

export default function NotFound() {
  return (
    <main className="flex min-h-[70vh] items-center justify-center px-6">
      <div className="text-center">
        <p className="text-sm font-medium text-blue-600">404</p>
        <h1 className="mt-2 text-4xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-5xl">
          Page not found
        </h1>
        <p className="mt-4 text-lg text-gray-500 dark:text-gray-400">
          The page you are looking for does not exist or has been moved.
        </p>
        <div className="mt-8 flex items-center justify-center gap-4">
          <Link
            href="/"
            className="rounded-lg bg-blue-600 px-6 py-3 text-sm font-medium text-white hover:bg-blue-700"
          >
            Go home
          </Link>
          <Link
            href="/contact"
            className="rounded-lg border px-6 py-3 text-sm font-medium hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800"
          >
            Contact us
          </Link>
        </div>
      </div>
    </main>
  );
}

Step 2: Add a Search Bar

Help visitors find what they were looking for:

"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

function NotFoundSearch() {
  const [query, setQuery] = useState("");
  const router = useRouter();

  function handleSearch(e: React.FormEvent) {
    e.preventDefault();
    if (query.trim()) {
      router.push(`/blog?search=${encodeURIComponent(query)}`);
    }
  }

  return (
    <form onSubmit={handleSearch} className="mt-8 mx-auto max-w-md">
      <div className="flex gap-2">
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search our site..."
          className="w-full rounded-lg border px-4 py-2 text-sm dark:border-gray-700 dark:bg-gray-800"
        />
        <button
          type="submit"
          className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
        >
          Search
        </button>
      </div>
    </form>
  );
}

Step 3: Add Popular Links

Guide visitors to your most valuable content:

const popularLinks = [
  { label: "Our Services", href: "/services", description: "See what we offer" },
  { label: "Blog", href: "/blog", description: "Read our latest articles" },
  { label: "Pricing", href: "/pricing", description: "View our plans" },
  { label: "Contact", href: "/contact", description: "Get in touch" },
];

function PopularLinks() {
  return (
    <div className="mt-12">
      <h2 className="text-sm font-semibold text-gray-900 dark:text-white">
        Popular pages
      </h2>
      <ul className="mt-4 divide-y dark:divide-gray-700">
        {popularLinks.map((link) => (
          <li key={link.href}>
            <Link
              href={link.href}
              className="flex items-center justify-between py-3 text-sm hover:text-blue-600"
            >
              <div>
                <span className="font-medium">{link.label}</span>
                <span className="ml-2 text-gray-400">{link.description}</span>
              </div>
              <svg
                className="h-4 w-4 text-gray-400"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
                strokeWidth={2}
              >
                <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
              </svg>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

Step 4: Full 404 Page

// app/not-found.tsx
import Link from "next/link";
import { NotFoundSearch } from "@/components/NotFoundSearch";

export default function NotFound() {
  return (
    <main className="flex min-h-[70vh] items-center justify-center px-6">
      <div className="mx-auto max-w-lg text-center">
        {/* Large 404 number */}
        <div className="text-[120px] font-bold leading-none text-gray-100 dark:text-gray-800">
          404
        </div>

        <h1 className="-mt-8 text-2xl font-bold text-gray-900 dark:text-white">
          Page not found
        </h1>
        <p className="mt-3 text-gray-500 dark:text-gray-400">
          Sorry, we could not find the page you are looking for. It may have been
          moved or deleted.
        </p>

        {/* Search */}
        <NotFoundSearch />

        {/* Actions */}
        <div className="mt-6 flex items-center justify-center gap-4">
          <Link
            href="/"
            className="rounded-lg bg-blue-600 px-6 py-2.5 text-sm font-medium text-white hover:bg-blue-700"
          >
            Go home
          </Link>
          <Link
            href="/contact"
            className="text-sm font-medium text-blue-600 hover:text-blue-700"
          >
            Contact support
          </Link>
        </div>

        {/* Popular links */}
        <PopularLinks />
      </div>
    </main>
  );
}

Step 5: Route-Specific Not Found Pages

Create not-found.tsx in specific route segments:

// app/blog/not-found.tsx
import Link from "next/link";

export default function BlogNotFound() {
  return (
    <div className="py-20 text-center">
      <h1 className="text-2xl font-bold">Article Not Found</h1>
      <p className="mt-2 text-gray-500">
        This blog post may have been removed or the URL is incorrect.
      </p>
      <Link
        href="/blog"
        className="mt-6 inline-block rounded-lg bg-blue-600 px-6 py-2 text-sm text-white"
      >
        Browse all articles
      </Link>
    </div>
  );
}

Trigger it from server components:

import { notFound } from "next/navigation";

export default async function BlogPost({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);

  if (!post) {
    notFound();
  }

  return <article>{/* post content */}</article>;
}

Step 6: Track 404 Errors

Log 404 hits to identify broken links and redirect opportunities:

// app/not-found.tsx
import { headers } from "next/headers";

export default async function NotFound() {
  const headersList = await headers();
  const referer = headersList.get("referer");

  // Log to analytics (server-side)
  console.log("404 hit:", {
    referer,
    timestamp: new Date().toISOString(),
  });

  return (
    <main>
      {/* 404 content */}
    </main>
  );
}

Design Tips

  • Keep the page on-brand (use your regular layout and navigation)
  • Use a clear headline that explains what happened
  • Provide a search bar and popular links
  • Include a direct path back to the homepage
  • Consider humor only if it fits your brand
  • Make sure the page returns a proper 404 HTTP status code
  • Test for accessibility

Need Website Design Help?

We design engaging websites with thoughtful error handling and user experience. Contact us to improve your site.

404 pageNext.jserror handlingUXtutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles