Not Found Page for React
Animated 404 page with subtle icon motion and reduced-motion support.
Component
"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.
import { NotFoundPage } from "@/components/not-found-page";
export default function NotFound() {
return <NotFoundPage />;
}Custom text
Customize text and home link.
import { 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.
import { 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 25