Not Found Page for React
Animated 404 page with subtle icon motion and reduced-motion support.
Component
404
Oops! Page not found
The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.
Back to Home"use client";
import { motion, useReducedMotion } from "framer-motion";
import { ArrowLeft, Frown } from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
interface NotFoundPageProps {
className?: string;
homeHref?: string;
title?: string;
description?: string;
helperText?: string;
backLabel?: string;
icon?: React.ReactNode;
buttonClassName?: string;
}
export function NotFoundPage({
className,
homeHref = "/",
title = "404",
description = "Oops! Page not found",
helperText = "The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.",
backLabel = "Back to Home",
icon,
buttonClassName,
}: NotFoundPageProps) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{
opacity: shouldReduceMotion ? 1 : 0,
y: shouldReduceMotion ? 0 : 20,
}}
animate={{ opacity: 1, y: 0 }}
transition={
shouldReduceMotion
? { duration: 0 }
: { duration: 0.5 }
}
className={cn(
"flex min-h-[60svh] flex-col items-center justify-center space-y-6 text-center",
className,
)}
>
<motion.div
animate={
shouldReduceMotion
? { rotate: 0 }
: { rotate: [0, 5, -5, 0] }
}
transition={
shouldReduceMotion
? { duration: 0 }
: {
duration: 2,
ease: "easeInOut",
repeat: Number.POSITIVE_INFINITY,
}
}
className="inline-block"
>
{icon ?? (
<Frown className="mx-auto h-24 w-24 text-muted-foreground" />
)}
</motion.div>
<h1 className="font-bold text-4xl text-foreground">
{title}
</h1>
<p className="text-muted-foreground text-xl">
{description}
</p>
<p className="mx-auto max-w-md text-muted-foreground">
{helperText}
</p>
<Link
href={homeHref}
className={cn(
"mt-4 inline-flex items-center rounded-md bg-primary px-4 py-2 font-medium text-primary-foreground text-sm transition-colors hover:bg-primary/90",
buttonClassName,
)}
>
<ArrowLeft className="mr-2 h-4 w-4" />
{backLabel}
</Link>
</motion.div>
);
}Installation
1. Install dependencies
pnpm add framer-motion lucide-react2. Copy the component file
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { ArrowLeft, Frown } from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
interface NotFoundPageProps {
className?: string;
homeHref?: string;
title?: string;
description?: string;
helperText?: string;
backLabel?: string;
icon?: React.ReactNode;
buttonClassName?: string;
}
export function NotFoundPage({
className,
homeHref = "/",
title = "404",
description = "Oops! Page not found",
helperText = "The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.",
backLabel = "Back to Home",
icon,
buttonClassName,
}: NotFoundPageProps) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{
opacity: shouldReduceMotion ? 1 : 0,
y: shouldReduceMotion ? 0 : 20,
}}
animate={{ opacity: 1, y: 0 }}
transition={
shouldReduceMotion
? { duration: 0 }
: { duration: 0.5 }
}
className={cn(
"flex min-h-[60svh] flex-col items-center justify-center space-y-6 text-center",
className,
)}
>
<motion.div
animate={
shouldReduceMotion
? { rotate: 0 }
: { rotate: [0, 5, -5, 0] }
}
transition={
shouldReduceMotion
? { duration: 0 }
: {
duration: 2,
ease: "easeInOut",
repeat: Number.POSITIVE_INFINITY,
}
}
className="inline-block"
>
{icon ?? (
<Frown className="mx-auto h-24 w-24 text-muted-foreground" />
)}
</motion.div>
<h1 className="font-bold text-4xl text-foreground">
{title}
</h1>
<p className="text-muted-foreground text-xl">
{description}
</p>
<p className="mx-auto max-w-md text-muted-foreground">
{helperText}
</p>
<Link
href={homeHref}
className={cn(
"mt-4 inline-flex items-center rounded-md bg-primary px-4 py-2 font-medium text-primary-foreground text-sm transition-colors hover:bg-primary/90",
buttonClassName,
)}
>
<ArrowLeft className="mr-2 h-4 w-4" />
{backLabel}
</Link>
</motion.div>
);
}3. Use in app/not-found.tsx
import { NotFoundPage } from "@/components/not-found-page";
export default function NotFound() {
return <NotFoundPage />;
}Usage
Import
Add the NotFoundPage import.
import { NotFoundPage } from "@/components/not-found-page";Use
Use in app/not-found.tsx.
export default function NotFound() {
return <NotFoundPage />;
}Guidelines
- Use in app/not-found.tsx (Next.js App Router) for the 404 route.
- Requires next/link; ensure you are in a Next.js project.
- Icon wobble animation on 404 page.
Props
All props are optional unless marked required. Use these to customize every aspect of the component.
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | Additional CSS classes. |
| homeHref | string | "/" | URL for the back/home link. |
| title | string | "404" | Main heading text. |
| description | string | "Oops! Page not found" | Subheading text. |
| helperText | string | "The page you are looking for..." | Helper paragraph text below the description. |
| backLabel | string | "Back to Home" | Label for the back link. |
| icon | React.ReactNode | <Frown /> | Custom icon rendered above the title. Accepts any React element - lucide icon, SVG, or emoji. |
| buttonClassName | string | - | Additional CSS classes for the back/home button. |
Accessibility
- The only interactive control is a next/link styled as a button, so it is natively keyboard focusable and operable with Enter.
- Focus visibility relies on the browser default outline since no custom focus-visible ring is defined; consider adding one to match the button styling.
- The heading uses a semantic h1 and the link contains visible text from backLabel, so its purpose is announced to screen readers without extra labels.
- No explicit ARIA roles, aria-label, or live-region announcements are present; the layout depends on visible text and heading semantics alone.
- Reduced motion is honoured via useReducedMotion, which disables the entrance fade-and-slide and the continuous icon wobble for users who prefer reduced motion.
Performance
- Animations are limited to opacity and CSS transform (translateY and rotate), which are compositor-friendly and avoid layout or paint thrashing.
- The icon wobble runs as an infinite Framer Motion loop driven by requestAnimationFrame, so it keeps running while mounted but stays on the GPU-accelerated transform property.
- When prefers-reduced-motion is set, durations collapse to zero and the infinite repeat is removed, eliminating ongoing animation work.
- Framer Motion manages its own animation lifecycle and cleanup on unmount, so there are no manual timers, listeners, or observers to leak.
- This is a small client component with no data fetching or state beyond the reduced-motion check, keeping its runtime cost minimal.
Examples
Basic
Use in app/not-found.tsx for Next.js App Router.
404
Oops! Page not found
The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.
Back to Homeimport { NotFoundPage } from "@/components/not-found-page";
export default function NotFound() {
return <NotFoundPage />;
}Custom text
Customize text and home link.
404
Oops! Page not found
The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.
Back to Homeimport { NotFoundPage } from "@/components/not-found-page";
export default function NotFound() {
return (
<NotFoundPage
homeHref="/"
title="404"
description="Page not found"
backLabel="Go home"
/>
);
}Custom icon
Pass any React element as the icon - a lucide icon, custom SVG, or emoji.
404
Oops! Page not found
The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.
Back to Homeimport { Ghost } from "lucide-react";
import { NotFoundPage } from "@/components/not-found-page";
export default function NotFound() {
return (
<NotFoundPage
icon={
<Ghost className="mx-auto h-24 w-24 text-muted-foreground" />
}
/>
);
}Related reading
- SEO for a Next.js Portfolio — Why a good 404 matters for crawl health
- Static Site Generation in Next.js — Statically generating error pages
Last updated on Jun 19