ZB Field Notes

I Turned My Website Into a Design System for Claude

I Turned My Website Into a Design System for Claude

I'm fond of how zakaria.lu looks. It's a deliberate aesthetic — a precision engineering-datasheet: bone paper, near-black ink, exactly one vermilion signal colour, and three type voices each doing one job. I wanted every visual I make from now on — product screens, a slide deck, the odd diagram — to carry that same DNA. So I spent a session teaching Claude Design (claude.ai/design) to build with my actual brand, and walked away with a real component library too. Here's how it went.

The site masthead: boxed ZB monogram, numbered nav, contact and sign-in actions
The masthead, rebuilt as a component — boxed monogram, a mono "specification / reference" lockup, numbered nav, a pulsing live dot.

The brand is a token system, not a logo

The whole look is a handful of CSS custom properties. Get these right and everything downstream inherits the feel — there's no second accent colour to police, no font soup:

--paper:  #ece4d6;   /* bone */
--ink:    #1a1714;
--signal: #d8381a;   /* the one accent */
--display:'Fraunces';       /* headlines — the soul */
--mono:   'IBM Plex Mono';  /* data, labels, captions */
--body:   'Hanken Grotesk'; /* prose */
A section header rendered in the brand: serif title, mono kicker, signal section number
Three jobs, three voices: a Fraunces display title, an IBM Plex Mono kicker, and the signal red marking the section number.

The catch: my site is an app, not a design system

Claude Design builds with components. My site doesn't have a component library — it's one 1,239-line App.tsx over an 1,810-line stylesheet, with buttons, cards, and rails inlined exactly where they're used. There was nothing tidy to hand over.

But those parts are real, shipped code; they were just never factored out. So the honest move wasn't to invent a kit — it was to extract one. I pulled the recurring patterns into a small, buildable package, @zakaria/brand-kit: 25 prop-driven components carrying the site's own markup and CSS, grouped into foundations, identity, typography, actions, display, and a motif.

A timeline entry: mono rail beside role, metric callout, bullets and stack chips
TimelineEntry: a mono rail beside a role, a single hard metric, arrow-led bullets, and stack chips. Same DOM as the live site — now it just takes props.

Two clean outputs, no magic bundler

The build does exactly two things, with tools I trust rather than a do-everything bundler:

  • esbuild emits one ESM file with React left external — the runtime the design agent imports.
  • tsc emits the .d.ts tree. That matters more than it sounds: the type declarations are the API contract the design agent codes against.
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'ghost'
  size?: 'md' | 'sm'
  arrow?: boolean   // the site's signature trailing arrow
}

I deliberately skipped the popular all-in-one bundler and pinned esbuild + tsc instead — fewer moving parts, no gamble on a toolchain-version mismatch, and I control exactly what ships in dist/.

Primary and ghost buttons, uppercase mono with a trailing arrow
The Button, both variants — uppercase mono, a hard offset shadow on hover, the trailing arrow that recurs across the site.

How Claude Design reads it, under the hood

This is the part I found genuinely clever. A design-system project on claude.ai/design isn't a Figma file — it's a small folder of code the platform consumes in a very specific way, and once you see the shape, the whole thing clicks. The upload is the contract:

  • The bundle is one IIFE. esbuild rolls every component into a single _ds_bundle.js that hangs each export off a global — window.<Namespace> — behind a first-line header comment the platform parses. When the design agent writes a screen, it's importing my real, compiled components off that global. Nothing is re-implemented; it runs the same code my site does.
  • Styles travel as one import closure. A root styles.css @imports the tokens, the webfonts, and the compiled component CSS. Every design the agent produces receives only that transitive closure plus the JS bundle — so anything not reachable from styles.css effectively doesn't exist for the output. That single rule is why tokens and component CSS have to be wired through it, not merely linked from a preview that never reaches a real design.
  • The .d.ts is the contract; the prompt doc is the manual. For each component the platform reads the TypeScript declarations as the API the agent must code against, and a generated usage doc as the reference for how to compose it. Get those wrong and the agent misuses the component everywhere — which is exactly why the build derives them from the real source instead of hand-waving them.
  • Preview cards register themselves. Each component ships an .html card whose first line is a tiny HTML-comment marker; a self-check scans for it to build the picker a human browses. React for those cards is vendored next to them so they render standalone, outside any app.
  • A self-check recompiles on open. The upload ends with a one-line sentinel file, written last so it fences the project while everything else lands. Opening the project trips a server-side pass that re-reads every .d.ts, registers the cards from their markers, regenerates the manifest from the uploaded source, and clears the sentinel. I never hand-write that manifest — the platform rebuilds it from what I shipped.

So there's no proprietary export format to fight: it's compiled components, type declarations, a stylesheet, and preview HTML, arranged so an agent and a human can both reach for the right part. The flip side of that elegance is unforgiving — a component that renders wrong here renders wrong in every design built from it later, which is what makes the verification step non-negotiable.

Verifying all 25 — with the browser already on the machine

A design system is only as trustworthy as its weakest card, so every component gets screenshotted in headless Chrome and graded before anything ships. The tooling wanted a fresh ~200 MB browser download; I already had Chrome installed, so I pointed Playwright straight at it:

# reuse the system Chrome instead of downloading a second one
export DS_CHROMIUM_PATH=".../Google/Chrome/Application/chrome.exe"
node package-validate.mjs ./ds-bundle   # => 25/25 previews render cleanly
A grid of component screenshots from the render check
Part of the render-check grid: every component rendered with real content and graded on an absolute rubric before upload.

I wrote each preview with real copy from the site — actual roles, the genuine stack, the real architecture decisions — never foo or bar. Those previews are what a human browses in the picker and what the agent imitates later, so placeholder text would quietly poison everything built on top.

A request-path flow diagram: Browser, Traefik, the app, Postgres
FlowNode and FlowArrow, composed into the site's real request path — Browser, Traefik, the container, Postgres — with the primary node highlighted.

A few honest deviations

Faithful doesn't mean photocopied. The live site lays a fixed, full-viewport grain texture over everything through a ::before overlay — lovely on a fixed page, but hostile inside arbitrary layouts, where it can paint over content. In the kit I baked the same noise into the page background with a multiply blend, so the texture sits behind content and composes safely. The masthead lost its position: fixed for the same reason. Small calls — written down in the repo so a future me knows they were on purpose, not bugs.

Two tall systems-matrix lanes with signal tags over serif titles
MatrixLane — the tall "operating model" cells, a signal mono tag pinned above a Fraunces title and detail.

What I actually walked away with

Two things. First, a design system on claude.ai/design: prompt the agent for a screen, a landing page, or a slide layout, and it now assembles them from my real parts in the right palette and type — and the result maps one-to-one onto code I can ship. Second, a genuine npm package, @zakaria/brand-kit, that any future product can install for a single source of truth.

One honest boundary, because I went in wanting "every visual": this makes the design tool produce on-brand work. It doesn't reach into an existing .pptx or a video file and repaint it. The way to get those on-brand is to regenerate them through the same engine — which, now that the engine knows my brand, is the easy part.

Pixel-art Claude mascot carrying a briefcase
And yes, the pixel mascot made the cut — ported verbatim, briefcase and all. Built with Claude.

Start to finish — extraction, build, 25 verified components, and the upload — was one focused session with Claude Code. The brand stopped being a thing only my website had, and became a thing I can hand to a tool.