Customize your 404 page
Start with the options; they cover colors, layout, radius, shadow and the button. When you need more, the library leaves stable class names on everything it renders, so your own CSS can take it the rest of the way.
Recipes
Each recipe is a complete call. Every one links to the playground with the same configuration loaded, so you can see it before you copy it. Replace /img/404-{n}.png with images you host.
Dark mode
RJLCustom404({
bodyBackgroundColor: "#0b1220",
bodyFontColor: "#e5e7eb",
subHeaderTextColor: "#94a3b8",
btnColor: "#00949e",
imgPattern: "/img/404-{n}.png",
imgCount: 6
}); Text left, image right
RJLCustom404({ headerTextPosition: "left", imgPattern: "/img/404-{n}.png", imgCount: 6 }); Stacked (text above the image)
RJLCustom404({ headerTextPosition: "top", maxImgWidth: "480px", imgPattern: "/img/404-{n}.png", imgCount: 6 }); Plain link instead of a button
RJLCustom404({ actionIsBtn: false, btnDisplayText: "Take me back to", imgPattern: "/img/404-{n}.png", imgCount: 6 }); Corporate, no motion
RJLCustom404({
headerText: "Page not found",
subHeaderText: "The page you requested may have been moved or removed.",
btnDisplayText: "Return to",
btnPulsate: false,
imgBorderRadius: "8px",
imgFileNames: ["/img/404.png"]
}); Playful with autoText
RJLCustom404({ autoText: true, imgBoxShadow: true, imgBoxShadowSize: "15px", imgPattern: "/img/404-{n}.png", imgCount: 6 }); Zero-padded file names
RJLCustom404({ imgPattern: "/img/404-{nn}.png", imgCount: 12 }); // 404-01.png ... 404-12.png Square image with hover shadow
RJLCustom404({ imgBorderRadius: "0", imgBoxShadow: true, imgBoxShadowColor: "rgba(0, 68, 148, 0.35)", imgFileNames: ["/img/404.png"] }); Send visitors somewhere more useful than the root
RJLCustom404({ homeUrl: "/docs/", btnDisplayText: "Browse the docs", btnDisplayHostName: false, imgFileNames: ["/img/404.png"] }); Use your site's font and tweak the button (CSS)
The library sets no font-family, so the page inherits whatever you put on body. It injects its own stylesheet when you call it, which lands after any static <style> in your page; use a slightly more specific selector to override the properties it sets.
<style>
body { font-family: Inter, system-ui, sans-serif; }
body .custom404-button { border-radius: 999px; font-weight: 600; }
body .custom404-header { font-size: 2.5em; letter-spacing: -0.02em; }
</style> CSS hooks
| Selector | Element | Notes |
|---|---|---|
.custom404-container | Outer flex wrapper appended to <body> | max-width: 1200px; direction follows headerTextPosition |
.custom404-img-container | Image column | max-width = maxImgWidth |
#goHomeLink | <a> wrapping the image | Points at homeUrl; only present when an image is configured |
.custom404-img | The <img> | border-radius set inline from imgBorderRadius; alt from imgAlt |
.custom404-img-missing | Bracketed placeholder <div> | Also carries data-custom404-missing="true" |
.custom404-text-container | Text column | Centered flex column |
.custom404-header | <h3> heading | font-size: 2em (1.8em at 768px and below, 1.5em at 480px and below) |
.custom404-subheader | <p> sub-heading | Only when subHeaderText is non-empty |
.custom404-button | <button> | Only when actionIsBtn: true; the plain link has no class |
style[data-custom404] | The injected stylesheet | Includes a global * { box-sizing: border-box } |
DOM structure
What the library appends, with the default headerTextPosition ("right": image first, then text). With "top" the text container comes first in the DOM; with "left" the DOM order stays image-then-text and flex-direction: row-reverse swaps them visually.
<head>
<style data-custom404="true">/* injected at call time */</style>
</head>
<body style="background-color: white; color: black; margin: 0; padding: 0; width: 100%; box-sizing: border-box;">
<div class="custom404-container">
<div class="custom404-img-container">
<a id="goHomeLink" href="/" title="Go to your-site.com">
<img class="custom404-img" src="/img/404-3.png" alt="404 Error" style="border-radius: 50%;">
</a>
<!-- when no image is configured, or the file fails to load: -->
<div class="custom404-img-missing" data-custom404-missing="true">[[ No 404 image configured ]]</div>
</div>
<div class="custom404-text-container">
<h3 class="custom404-header">404 - File not found</h3>
<p class="custom404-subheader">Only rendered when subHeaderText is set</p>
<button class="custom404-button" title="Go to your-site.com">Back to your-site.com</button>
<!-- with actionIsBtn: false -->
<a href="/" title="Go to your-site.com">Back to your-site.com</a>
</div>
</div>
</body> Right-click is disabled on the container and dragging is disabled on the image. If that matters to you, override it with your own listeners after the call.
JavaScript API
- RJLCustom404(options?)
- Renders the page. Calling it again removes the previous container and stylesheet and renders fresh, which is exactly how the playground re-renders on every change.
- RJLCustom404.refreshImage()
- Swaps in the next image from the shuffle queue. Does nothing while the placeholder is showing.
- RJLCustom404.version since 1.3.0
- The library version string, currently
1.3.0. Handy for support questions and for checking that a?v=bump actually took.
RJLCustom404({ imgPattern: "/img/404-{n}.png", imgCount: 6 });
// press "n" for the next image
document.addEventListener("keydown", (e) => {
if (e.key === "n") RJLCustom404.refreshImage();
});
console.log(RJLCustom404.version); // "1.3.0" localStorage
| Key | Written when | Value |
|---|---|---|
rjl404_<hostname>_image | Two or more images configured | { "queue": [remaining paths], "last": "<path>", "src": "<sorted path fingerprint>" } - the queue is discarded when the image set changes |
rjl404_<hostname>_text | autoText: true | { "titles": [], "headers": [], "subHeaders": [], "btnTexts": [] } - the remaining shuffled pools |
No cookies. Nothing is sent anywhere. If storage is unavailable (private mode, blocked) the shuffle still works within a page load; it just does not remember across reloads.
Browser support
Every evergreen browser: Chrome, Edge, Firefox and Safari, on desktop, iOS and Android. The source uses ES2017 features (let/const, template literals, spread, String.prototype.padStart), so Internet Explorer is not supported.
Changelog and migrating from 1.x
1.2.0 removed the bundled images (see Getting started: Upgrading). 1.3.0 added homeUrl, imgAlt and RJLCustom404.version without changing anything that already worked. The full release history is in CHANGELOG.md on GitHub.