Skip to content
2026Live and maintained

razi.fs

This website — a statically exported Next.js app with a hand-built 8-bit design system, MDX content, and a CRT phosphor you can switch.

nextjstypescripttailwinddesign

A personal site that refuses to look like a personal site. Every pixel of the interface is generated from CSS and hand-authored sprite data — no images, no icon library, no runtime.

What makes it work

The whole design system is 8-bit primitives. Fourteen CSS utilities do the visual heavy lifting: hard zero-blur shadows, notched corner cuts, a stepped animation curve, and a font stack of one pixel display face plus one readable mono for prose.

Icons are data, not files. 38 sprites live in a TypeScript file as character grids. Every pixel resolves to a CSS custom property, so switching the CRT phosphor recolours all 38 icons from one attribute on <html>.

There is no server. output: "export" means every route is prerendered to HTML at build time. The whole site is a folder of files that could be served from anywhere.

Content is MDX in the repo. Posts and projects are .mdx files with frontmatter. Reading time, tag counts, adjacent-post navigation and the sitemap are all derived at build time — there's no database to keep in sync.

Sound is synthesised. WebAudio square waves with hard attack envelopes. No audio files ship, and the whole SFX layer is about 80 lines.

Tech

| Layer | Choice | | --------- | --------------------------------------------- | | Framework | Next.js 16 (App Router, static export) | | Language | TypeScript, strict | | Styling | Tailwind CSS v4 with custom @utility rules | | Components| shadcn/ui, restyled square and hard-shadowed | | Content | MDX compiled at build time | | Icons | Hand-authored sprite grids → SVG |

Things I got wrong first

  • Passing variant straight through to shadcn's Button. The prop name collided with shadcn's own variant, and TypeScript caught it. Renaming mine to an explicit tone map fixed it.
  • clip-path killed every hard shadow. Notched frames now render the shadow as a second offset layer.
  • Unquoted YAML dates. gray-matter parses date: 2026-08-14 into a Date object, which is not a string and fails my frontmatter validation. Quote your dates.

Why it's built this way

A personal site should be a toy that happens to be useful. This one loads no analytics, sets no cookies, has no tracking pixel, and costs nothing to host — and the effort went into making it feel like a 1987 arcade cabinet instead of into a cookie banner.

It's also the single best portfolio piece I've ever made, because it's demonstrably the thing I know how to do.

HAVE A QUESTION?

Open an issue on the repository, or just email me. Both work.

Email Razi

MORE PROJECTS

Live

A zero-dependency WebAudio library that plays NES-style music from a compact note syntax. Ships about 4kb and no assets.

typescriptaudioweb-audioopen-source
LiveSource
Live

A browser tool that converts any image into 1-bit, 4-colour and 8-colour pixel art with real dithering algorithms. Fully client-side.

typescriptcanvastoolingwebgl
LiveSource
In progress

A habit tracker built as a text adventure. Quests, XP, stats, and a deliberately punishing stat decay that made me actually do the things.

typescriptreactsqlitedesign
Source

Command palette

Search for a command to run