Error 404
A not-found illustration where the zero in 404 breaks into two halves that drift apart, with a spark arcing across the gap.
Live preview and source code for Error 404
"use client"
import React from "react";
import { motion } from "framer-motion";
export interface Error404Props {
/** Rendered width of the illustration */
size?: number;
/** Accent colour for the plug, cable and highlights */
accent?: string;
/** Headline under the artwork. Pass `null` for artwork only */
title?: string | null;
/** Supporting line under the title */
description?: string | null;
/** Stops the drift and spark animations */
still?: boolean;
className?: string;
}
/**
* Error404 — a not-found illustration where the zero in "404" breaks into
* two halves that drift apart, with a spark arcing across the gap.
* Renders standalone with zero props.
*/
export default function Error404({
size = 300,
accent = "#7c3aed",
title = "Page not found",
description = "The link may be broken, or the page may have moved.",
still = false,
className = "",
}: Error404Props) {
return (
<div
className={className}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 6,
fontFamily: "inherit",
color: "#18181b",
textAlign: "center",
}}
>
<svg
width={size}
height={size * 0.6}
viewBox="0 0 300 180"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="404 — page not found"
>
<defs>
<linearGradient id="e404-num" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#27272a" />
<stop offset="100%" stopColor="#52525b" />
</linearGradient>
</defs>
<ellipse cx="150" cy="150" rx="96" ry="9" fill="#18181b" opacity="0.06" />
{/* The two 4s */}
<text
x="52"
y="120"
textAnchor="middle"
fontFamily="inherit"
fontSize="104"
fontWeight="700"
letterSpacing="-0.05em"
fill="url(#e404-num)"
>
4
</text>
<text
x="248"
y="120"
textAnchor="middle"
fontFamily="inherit"
fontSize="104"
fontWeight="700"
letterSpacing="-0.05em"
fill="url(#e404-num)"
>
4
</text>
{/* The 0, split into two halves that drift apart like a broken link */}
<motion.path
d="M150 42 A40 40 0 0 0 150 122"
fill="none"
stroke={accent}
strokeWidth="15"
strokeLinecap="round"
animate={still ? undefined : { x: [0, -7, 0] }}
transition={still ? undefined : { duration: 3.4, ease: "easeInOut", repeat: Infinity }}
/>
<motion.path
d="M150 42 A40 40 0 0 1 150 122"
fill="none"
stroke={accent}
strokeWidth="15"
strokeLinecap="round"
animate={still ? undefined : { x: [0, 7, 0] }}
transition={still ? undefined : { duration: 3.4, ease: "easeInOut", repeat: Infinity }}
/>
{/* Spark arcing across the gap as the halves separate */}
{!still && (
<motion.path
d="M144 62 L154 74 L145 78 L155 90"
fill="none"
stroke="#f59e0b"
strokeWidth="3.5"
strokeLinecap="round"
strokeLinejoin="round"
animate={{ opacity: [0, 0, 1, 0, 0] }}
transition={{ duration: 3.4, repeat: Infinity, times: [0, 0.35, 0.5, 0.62, 1] }}
/>
)}
</svg>
{title && (
<span style={{ fontSize: 16, fontWeight: 600, letterSpacing: "-0.01em" }}>{title}</span>
)}
{description && (
<span style={{ fontSize: 13.5, color: "#71717a", maxWidth: 300, lineHeight: 1.55 }}>
{description}
</span>
)}
</div>
);
}
A not-found illustration for a 404 page: the zero in the middle of the number is split into two halves that drift apart, with a spark arcing across the gap like a broken connection. The digits are SVG text in the page's own font, so it matches your type rather than looking like a stock graphic, and the accent colour is a prop. Title and description sit underneath and can be replaced or removed.
Installation
Copy the source from the Code tab above into your project as Error404.tsx.
The artwork is inline SVG and the copy below it is styled inline, so there is
no image to host and no stylesheet to import.
It imports framer-motion for the drift and the spark:
npm install framer-motionRendered with still, nothing animates, so a static-only copy can drop that
import and turn the two motion.path halves of the zero into plain path
elements.
Usage
Every prop is optional. size is the rendered width in pixels and the height
follows at 60 percent of it:
import Error404 from "./components/Error404";
export default function Example() {
return (
<Error404
size={300}
accent="#7c3aed"
title="Page not found"
description="The link may be broken, or the page may have moved."
/>
);
}Pass title={null} and description={null} for the number on its own, and
still to stop the drift and hide the spark.
When to reach for it
The not-found route of a site, where the page needs to say something went
missing without looking like a crash. The drifting halves read as a broken link
rather than an error.
A dead or expired share link, where the useful message is that the address no longer resolves.
An empty routing state in a dashboard, at a smaller size, sitting above a
link back to somewhere real.
When not to
If the server failed. A 500, a timeout or a thrown error is a different message: something on your side broke and retrying may work. This says the address is wrong, which sends the reader looking in the wrong place.
If a search or filter returned nothing. That is a zero state, not a missing page. Empty Search is the one for that.
If you need the number to be selectable text. The digits are SVG text inside an image role, so they cannot be copied and are not part of the page's text content.
Accessibility
The SVG carries role="img" and a label naming the page as not found, so it is
announced once as a picture and the loose digits inside it never reach the
reading order individually. The title and description below are plain text and
read normally.
That label is a literal string in the source. Changing title or translating
the page does not update it, so edit the aria-label in the copied file when
the language changes.
The drift and spark loop forever and do not check prefers-reduced-motion.
Pass still from your own media query check if you honour that setting.
Notes
The digits use font-family: inherit, which is what makes them match the page,
and also means the spacing depends on your font. The two 4s are positioned at
fixed coordinates either side of the zero, so a very wide typeface can crowd
the gap. Check it once with your own font before shipping.
The gradient in <defs> is declared with the id e404-num. Two of these on
one page would duplicate that id, so rename it in the copied file if that
happens.
still hides the spark rather than freezing it mid-arc, so the still version
is the number with a clean gap.
The digit colours are a fixed dark gradient built for a light background. On a dark page the accent still works but the 4s need recolouring in the source.