Building a CRT arcade portfolio with Astro

Every developer portfolio looks the same: hero, grid of cards, contact form. I wanted mine to feel like something instead — specifically, like the black and white arcade cabinets that started this whole industry. This post is a tour of how the illusion works.

The constraint that does the heavy lifting

The whole site obeys one rule: white phosphor on black glass. No accent colours, no gradients, no photos. That single constraint does more design work than any framework could — every element has to earn its place with shape and motion instead of colour.

The stack is deliberately boring where it can be: Astro renders everything to static HTML, React islands hydrate only the interactive parts, and Tailwind keeps the styling in one place. The exotic bits — PixiJS, Kaplay, typed.js — load lazily inside the islands that need them.

Pixel sprites as data

Every sprite on the site — invaders, ships, the UFO, social icons, my avatar — is a tiny string array:

export const CRAB_A: PixelMap = [
  "..X.....X..",
  "...X...X...",
  "..XXXXXXX..",
  ".XX.XXX.XX.",
  "XXXXXXXXXXX",
  "X.XXXXXXX.X",
  "X.X.....X.X",
  "...XX.XX...",
];

One mapToPixels() helper turns the Xs into coordinates, and from there the same map renders as inline SVG (<rect> per pixel), as a PixiJS texture, or as a data-URL for the Kaplay game. Drawing a new sprite is editing text — which is exactly how it felt on 8-bit hardware.

Faking a CRT in CSS

The monitor effect is three stacked layers, all pointer-events: none:

  1. A static layer — scanlines, an aperture grille and a vignette, merged into one element with three background-image gradients.
  2. A flicker layer — near-transparent white, stepping between opacities at ~8 Hz.
  3. A sweep band — a soft horizontal highlight that travels down the screen every nine seconds, like a badly shielded tube.

Phosphor colour themes (white, amber, green) are one CSS variable and a mix-blend-mode: multiply overlay — the whole site recolours at once, no per-component work.

Details that sell it

  • Boot sequence. First visit plays a memory check and waits at PRESS START; like a real cabinet’s attract mode, it starts on its own if you idle. (That also keeps search-engine crawlers, which never click, from seeing an empty page.)
  • Sound. Every bleep is a raw Web Audio oscillator — square and sawtooth waves with pitch sweeps. No audio files, and it’s off by default.
  • Terminal. Press ` and a DOS-style prompt opens: navigate, switch themes, or rm things that refuse to be deleted.
  • Games. The hero backdrop is PixiJS — the invaders dodge, flee and shoot back when clicked. The bonus stage is a full Space Invaders in Kaplay with power-ups tied to my actual skills, plus an arcade top-5 with initials.

One codebase, two languages, one URL tree each

Every meaningful string renders twice — <span data-l="en"> and <span data-l="sk"> — and CSS shows the active one. English lives at /, Slovak at /sk/, each with visible text at build time and hreflang pairs, so both versions are honestly indexable. The language toggle just navigates to your twin URL.

The CV that prints itself

The /cv page is the same design system with a twist: all colours are CSS variables, and a print stylesheet flips them from phosphor-on-black to ink-on-paper. A small script drives headless Chrome’s --print-to-pdf over the built site, so the downloadable PDFs are pixel-identical to what the browser would print — no PDF library involved.

Was it worth it?

The site is still static HTML that loads fast and reads fine without JavaScript. Everything on top — the glow, the games, the bleeps — is progressive enhancement wearing a 1978 costume. Building it taught me more about CSS blend modes, Web Audio and browser print engines than any tutorial could.

Insert coin to continue.

SLIVOTRON-2000 · DEV LOG