A 404 page that's actually fun to land on

Why I built a CSS-only animated 404 with tilting digits, an orbiting astronaut, and a few dad jokes — and what it taught me about Astro's static 404 conventions.

A 404 page that's actually fun to land on

I built a proper 404 page this weekend. Not because analytics were yelling at me — nobody really hits dead links on a five-page portfolio — but because the default “Page not found” annoyed me every time I clicked through to it.

Why bother

If you’ve landed on a 404, you’re already mildly grumpy. You typed something wrong, followed a stale link, or an AI answer sent you to a path that never existed. The page has exactly one job there: don’t pile on, and give them a way out.

What I don’t like about most “fun” 404s: it’s either a wall of corporate apology, or a giant illustration with no button. I wanted the middle ground — a short joke, then two clear doors back.

The copy

The voice is the same one the rest of the site uses. The portfolio is direct and a bit playful, it’s not a bank — so the 404 sounds like that too:

  • Eyebrow: TRANSMISSION LOST · ERROR 404
  • Headline: This page got abducted by aliens.
  • Body: We searched every route. Even peeked at /api/secret-cookies.
  • Closer: Either the URL never existed, or you just discovered a wormhole. Honestly, pretty cool either way.

The German version isn’t a literal translation — it just had to land the same energy. Diese Seite wurde von Aliens entführt. does the job. /api/secret-cookies became /api/geheime-kekse, and Beam me home turned into Zurück zur Erde — a bit more Star Trek on the EN page, a bit more grounded on the DE one.

Both strings live under notfound.* in en.json / de.json, like every other piece of copy on the site. They get the data-scramble effect from the nav for free.

The animation

Pure CSS. No JS, no GSAP. The page already runs under PortfolioLayout, which deliberately doesn’t load the Webflow IX2 runtime — and I wasn’t going to drag in a heavy animation library for a page most users will never see.

What the scene is made of:

  • A big 404 in Archivo Black. The two 4s tilt in opposite directions and breathe up-down on a five-second loop.
  • The 0 isn’t really a digit anymore — it’s a div with a radial gradient running from amber through red into dark oxide, a layer of atmospheric bands on top, and a slow brightness breathe. Reads as a tiny planet.
  • A 2px ring sits at -18° on the zero, slightly squashed. Enough to hint at Saturn.
  • An SVG astronaut (helmet, visor reflection, “404” chest patch, red antenna light) orbits the scene. The orbit container rotates clockwise, the astronaut counter-rotates so its head stays up, and a separate bob animation gives the zero-G float.
  • Two starfields built from radial-gradient with offset twinkle timings (4s and 7s). Doesn’t cost a single asset.
@keyframes nf-orbit {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

@keyframes nf-counter-spin {
  from { transform: translateX(-50%) rotate(0deg); }
  to   { transform: translateX(-50%) rotate(-360deg); }
}

All of it sits behind a prefers-reduced-motion guard. Anyone with that set in their OS gets the same layout, just frozen. Nobody should get motion sick on a 404.

Astro and the 404 quirk

One thing that briefly tripped me up: Astro emits src/pages/404.astro as dist/404.html (flat), even when build.format is set to directory for the rest of the site. Cloudflare Pages, Netlify, Vercel — they all pick that file up as the site-wide 404 automatically.

src/pages/de/404.astro falls back to the directory rule and ends up as dist/de/404/index.html. How each host handles nested 404s varies — I’ll wire the German fallback through a _redirects rule when I get to it. Until then, an unknown URL anywhere on the site lands on the English 404, which is fine for v1.

What stuck

A small CSS-only scene reads as more considered than a giant Lottie, and ships in roughly 4 KB of style. Keep the joke as short as possible — one headline, one body line, one closer, done. And always ship the two buttons back, otherwise the joke stops being charming around the third read.

If you want to see it: just make up a path. Try /abducted-by-aliens — that’s exactly what it’s there for.