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:
- A static layer — scanlines, an aperture grille and a vignette, merged
into one element with three
background-imagegradients. - A flicker layer — near-transparent white, stepping between opacities at ~8 Hz.
- 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, orrmthings 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.