EmpireUI
Get Pro
← Blog7 min read#error-page#dark-ui#404-design

Dark Error Page Design: 404, 500, Maintenance UI Variants

Dark error pages don't have to be an afterthought. Here's how to design 404, 500, and maintenance screens that actually fit your UI system.

Dark terminal screen with error code displayed in a dimly lit workspace

Why Dark Error Pages Matter More Than You Think

Honestly, error pages are the most neglected screens in any product — and yet they're the ones users see right when things go wrong. That's the worst possible moment to break visual consistency.

A 404 that looks like it was built in 2014 with a white background and a Times New Roman heading is a trust killer. Users don't consciously register it as "bad design" — they just feel uneasy. That unease sticks.

Dark error pages specifically matter because most modern SaaS and developer tools already lean toward dark themes. If your app is dark and your 404 is blinding white, that's a jarring context switch. You're forcing the user's eyes to adjust at exactly the moment they're already frustrated.

This guide walks through practical design approaches for 404, 500, and maintenance pages — with real Tailwind v4.0.2 examples you can drop into a Next.js app today. No theory-heavy fluff, just components that work.

The Three Error States You Actually Need to Design

Most teams design exactly one error page, slap the same layout on all three cases, and ship it. That's fine, but there's a real opportunity to communicate differently depending on what went wrong.

A 404 means the user ended up somewhere that doesn't exist. They navigated wrong, clicked a stale link, or you changed a URL without a redirect. The tone here should be slightly forgiving — almost playful if your brand allows it. Give them an obvious escape route back to somewhere useful.

A 500 is different. Something broke on your end. The tone shifts. You don't want playful — you want honest and calm. Users need to know this isn't their fault, and ideally, that you're aware of it. A status page link or a "we've been notified" message goes a long way.

Maintenance mode is the most controlled of the three. You chose this downtime. The design should feel deliberate — a countdown timer, an estimated return time, maybe a progress indicator. If you're building a theme toggle in React, maintenance pages are actually a great place to test dark/light switching since they're isolated components.

Core Dark UI Color Tokens for Error Screens

Before writing a single component, you need to lock down a small set of color tokens. Error pages are standalone screens — they often load outside your normal layout — so they can't always inherit your design system globals. They need to be self-contained.

For a dark error page, you want a background that reads as intentionally dark, not just "the CSS failed to load." There's a difference between #0a0a0a (intentional) and #1a1a1a (looks like a broken page). Adding a subtle radial gradient helps — something like radial-gradient(ellipse at 30% 20%, rgba(99,102,241,0.12) 0%, transparent 60%) gives depth without being loud.

Foreground text should sit around rgba(255,255,255,0.85) for body copy and rgba(255,255,255,0.45) for secondary labels. Error codes — the big "404" heading — can go full white or use a muted accent color. Avoid pure red for 500 errors; it reads as alarming. A desaturated amber or a cool slate works better for "something broke but we're on it."

Building a Dark 404 Component in React and Tailwind

Here's a minimal but complete 404 component. It uses Tailwind v4.0.2 utility classes and a small inline style for the gradient background. No external dependencies.

// components/errors/NotFound404.tsx
import Link from 'next/link'

export default function NotFound404() {
  return (
    <main
      className="min-h-screen flex flex-col items-center justify-center text-white px-6"
      style={{
        background:
          'radial-gradient(ellipse at 30% 20%, rgba(99,102,241,0.12) 0%, transparent 60%), #0a0a0a',
      }}
    >
      <span className="text-sm font-mono tracking-widest text-white/40 uppercase mb-4">
        Error
      </span>
      <h1 className="text-[9rem] font-black leading-none tracking-tighter text-white/90">
        404
      </h1>
      <p className="mt-4 text-lg text-white/60 max-w-md text-center">
        That page doesn't exist — or it moved and we forgot to leave a note.
      </p>
      <div className="mt-10 flex gap-4">
        <Link
          href="/"
          className="px-6 py-3 rounded-lg bg-white/10 hover:bg-white/15 border border-white/10 text-sm font-medium transition-colors"
        >
          Go home
        </Link>
        <Link
          href="/components"
          className="px-6 py-3 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-medium transition-colors"
        >
          Browse components
        </Link>
      </div>
    </main>
  )
}

A few things worth noting in that code. The text-[9rem] is an arbitrary value — Tailwind v4 handles these cleanly without needing to extend your config. The bg-white/10 and border-white/10 use Tailwind's opacity modifier syntax, which maps to rgba(255,255,255,0.10) under the hood. That 8px gap between buttons (gap-4 = 16px) is intentional — tighter looks cramped on a sparse page.

500 and Maintenance Variants: Adapting the Base Layout

The 404 layout above is your base. For a 500, you mainly change three things: the error code, the copy, and the accent color. Swap the indigo gradient for something like rgba(245,158,11,0.10) — that's a muted amber that reads as "caution" without being alarming. The CTA changes too; instead of "Go home" you might show "Check status page" linking to your status.io or BetterStack URL.

Maintenance pages benefit from a slightly different structure. You want a visible signal that this is temporary and intentional — not a crash. Adding an estimated time helps: even a rough "back in ~20 minutes" is better than silence. If you're using a design system that handles glassmorphism effects, a frosted glass card containing the maintenance message can look particularly polished on a dark background.

One pattern that works well: a horizontal progress bar that pulses or animates slowly. It doesn't have to represent real progress — it just communicates "something is happening." A simple CSS animation using @keyframes and width transition does the job without pulling in a library.

Is it worth building all three variants separately? Honestly, yes. They're small components. The props API is nearly identical — you pass in a code, title, description, and optional cta prop. Three files, maybe 60 lines total. The time investment is minimal and the UX improvement is real.

Styling Approaches: Glassmorphism, Neobrutalism, and Flat Dark

Error pages are actually a good place to experiment with visual style because they're low-stakes UI — they're rare, standalone, and users don't interact with them for long. That said, the style you pick should match your app's overall visual language, not contradict it.

Flat dark is the safest default. Dark background, clean typography, minimal decoration. It works for developer tools, CLI wrappers, anything technical. The 404 component above is flat dark.

Glassmorphism works if your app already uses it. A frosted card with backdrop-filter: blur(12px) and background: rgba(255,255,255,0.06) sitting on top of a blurred or gradient background. Just be careful — glassmorphism vs neumorphism is an interesting comparison here, because neumorphic error pages can feel oddly cheerful given the context. Soft shadows and embossed text don't scream "something went wrong."

Neobrutalism is the outlier. Bold borders, high contrast, stark typography. It can work for a 404 if your whole product uses neobrutalist design — but it almost never works for a 500. A brutal error message on a 500 page just feels mean. Match the emotional tone of the error state to the visual energy of the style.

Accessibility and Motion Considerations

Error pages often get skipped in accessibility audits because they're not in the main user flow. That's a mistake. If someone can't navigate back to your app from a broken URL, you've created a real dead end.

Make sure your primary CTA button has sufficient contrast — rgba(255,255,255,0.10) backgrounds on white text pass at large sizes but can fail on small body copy. Run your error page palette through a contrast checker. Aim for at least 4.5:1 on interactive text.

If you're adding animation — pulsing progress bars, floating particles via particles background, or entrance transitions — always respect prefers-reduced-motion. Wrap animations in a media query or use Tailwind's motion-safe: modifier. An error page that bounces and pulses for someone with vestibular sensitivities is worse than a static one.

Tab order matters too. On a page with just two links and no nav, getting the focus ring right is trivial — but don't skip it. The default Tailwind focus ring (ring-2 ring-offset-2 ring-indigo-500) works well on dark backgrounds without any customization.

Fitting Error Pages Into Your Next.js App Router Structure

In Next.js 14+ with the App Router, you get dedicated file conventions for error states: not-found.tsx for 404s and error.tsx for runtime errors. Maintenance mode is slightly different — you'd typically handle that at the middleware or CDN level, but you can also manage it with a simple environment variable check in your root layout.

The error.tsx boundary requires a Client Component ('use client') because it needs access to the reset function. That's fine — your visual layer can still be server-safe, just wrap it. Keep the actual design component in a separate file so you can import it in both error.tsx and not-found.tsx without duplicating markup.

One thing worth knowing: Next.js generates static 404 pages at build time. If your dark theme relies on a CSS variable set by JavaScript (common with Tailwind CSS modules patterns), the static 404 might flash light before the JS runs. Hardcode the dark background directly in the component's style prop — don't rely on a class set by a theme toggle — and you'll avoid the flash entirely.

FAQ

Can I use the same dark error page component for both 404 and 500 in Next.js?

Yes, and it's a common pattern. Create a base ErrorPage component that accepts code, title, description, and optional cta props. Then import it in both not-found.tsx and error.tsx with different prop values. The 500 version needs to be a Client Component because error.tsx requires 'use client', but the component itself can be written as a shared presentational component.

How do I prevent a flash of white on a static Next.js 404 page when using dark mode?

Don't rely on a class applied by JavaScript for the dark background on error pages. Instead, set the background color directly in the component's inline style prop — something like style={{ background: '#0a0a0a' }} — so it's present in the initial HTML. Next.js generates not-found.tsx as a static page, so any theme class applied via JS will arrive after the initial render.

What's the right background color for a dark 404 page?

There's no single right answer, but #0a0a0a or #0d0d0d works well as a near-black that reads as intentional. Adding a subtle radial gradient — like rgba(99,102,241,0.10) at the top-left corner — gives depth. Avoid pure #000000; it can look like a rendering failure rather than a designed screen.

Should my maintenance page be a different visual style than my 404?

They can share the same base design, but the messaging and tone should differ. A maintenance page should feel calm and temporary — include an estimated return time if you have one. A 404 can be slightly more casual. The visual difference is usually just copy and CTA — you don't need separate component architectures.

How do I handle `prefers-reduced-motion` for animated error pages in Tailwind?

Use Tailwind's built-in motion-safe: and motion-reduce: variants. For example: motion-safe:animate-pulse will only apply the pulse animation if the user hasn't requested reduced motion. For custom CSS animations, wrap them in @media (prefers-reduced-motion: no-preference) { ... }. Never auto-play looping animations on error pages without this guard.

Is it worth adding a status page link on 500 error pages?

Yes, absolutely. When your app throws a 500, users immediately wonder if the outage is widespread or just affecting them. A link to your status page — BetterStack, status.io, or even a simple hosted status site — answers that question without them having to search for it. It also signals that you take uptime seriously. Keep the link understated: secondary text size, not a primary CTA.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Neon Text Glow in CSS: Text-Shadow Techniques for Dark UIsDark Neobrutalism Cards: Bold UI for Developer ToolsComponent State Design: Default, Hover, Active, Disabled, ErrorResponsive Design Systems: Mobile-First Component Variants