Architecture
Self-contained Next.js app with scoped CSS and zero MDX
The system lives at design/ in the monorepo. A single Next.js 14 app with App Router, TypeScript everywhere, Tailwind v4 for utilities, and scoped CSS for the doc site. The portfolio, case studies, and system documentation are three deploys of the same codebase.
The doc system uses its own CSS file (doc-system.css) scoped under a single class (.ds) so it cannot collide with production portfolio styles. Every rule contributes to a small, opinionated, public design language.
Authoring is plain React. No MDX, no CMS, no Markdown parsing pipeline. Each page is a React component that imports doc primitives (DocShell, DocPageHeader,DocCallout, etc.) and writes the system in the system. The cost of authoring equals the cost of writing a component — that is the right shape.
Tokens
CSS variables exposed via Tailwind v4's @theme inline
Tokens are CSS custom properties declared once in app/globals.css and exposed to Tailwind via @theme inline. Both the production portfolio and the doc system read the same token values, so changing --foreground updates every page in every site at once.
Color, typography, radius, motion, and line-system tokens are all defined this way. No hard-coded color values exist in the codebase. Every reference goes through a token namespaced to the design language, not the implementation.
Token shape
:root {
--background: #f6f3ef; /* paper */
--foreground: #1a1816; /* ink */
--accent: #8a7d6e; /* warm taupe */
--border: #ddd7ce;
--line-subtle: rgba(26, 24, 22, 0.08);
--line-strong: rgba(26, 24, 22, 0.16);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
/* ...all tokens mapped to Tailwind utility colors... */
}Doc primitives
Twenty React components replace an entire doc runtime
Most design system documentation runs in a separate runtime — MDX, Storybook, a static site generator. That runtime almost always drifts from production code, and the docs become a screenshot museum.
Instead, the doc primitives are plain React components: DocShell, DocPageHeader, DocSection, DocProse, DocCallout, DocCodeBlock, DocDemo, DocSwatch, DocTypeRamp, DocSpacingStrip, DocRadiusGrid, DocShadowGrid, DocMotionRow, DocIconGallery, DocTable, DocDoDont. Twenty primitives used across fifty-plus pages.
The copy button on a code block copies real, current code. The button demo renders the actual button. The type ramp is the actual type scale. The spec is the implementation.
20+ doc primitives
Shell, page header, section, prose, callout, code block, demo, swatch, type ramp, spacing strip, radius grid, shadow grid, motion row, icon gallery, table, do/don't.
No MDX, no CMS
Every doc page is a plain React component. Authoring is a code review, not a content workflow. No separate runtime to maintain.
Same code that ships
The Button on the Button doc page is the Button in the production portfolio. The card in the patterns page is the card on the home page. The spec is the implementation.
Build & deploy
Static by default, dynamic only where needed
The system ships as a static Next.js build. Pages without runtime data are statically generated at build time. The only dynamic surfaces are the search command palette, the table-of-contents intersection observer, and the form action handlers.
First-load JS for the doc system stays bounded: the shared chunk is around 87 kB. The doc primitives are small, and prose-only pages have effectively no client-side JavaScript beyond the search trigger.
Two route aliases resolve to the same canonical content: /designsystem and/design-system. Both are configured in next.config.mjs via App Router rewrite rules.
Accessibility
WCAG AA decisions enforced in code, not decks
The system documents focus order, keyboard behavior, ARIA usage, reduced motion, and contrast at the CSS-variable level. Every interactive element is reachable with the keyboard, named with the right role, and operable with reduced motion.
Focus rings are visible, soft, and on-brand — they never disappear and are never replaced with color alone.
Accessibility checklist, in code
- 1Contrast: every pair tokenized and tested at WCAG AA. The CSS variable declarations are the receipt.
- 2Focus:
:focus-visiblewith a 2px outline and 2px offset. Nooutline: nonewithout a replacement. - 3Keyboard: every interactive primitive is a real button, link, or form control. No divs with onClick. No
role="button"on a div. - 4Reduced motion:
@media (prefers-reduced-motion: reduce)gates every keyframe and transition duration. Tested in Chromium and Safari. - 5ARIA: used to clarify, never to replace semantics. Roles, names, and states before attributes. The HTML <dialog> element for modals, not a custom role.
Stack
Minimal dependencies by design
Next.js 14 (App Router). Static by default, server components for prose, client components only for surfaces that need state (search, intersection observer, toasts). Route aliases for/designsystem and /design-system are next.config.mjs rewrites.
Tailwind v4 for utilities, scoped CSS for the doc system. Tailwind v4's@theme inline exposes CSS variables as utility classes without losing the underlying source of truth.
TypeScript everywhere. Doc primitives, data layer, navigation tree — all typed. The build is a type check; the type check is the spec.
Lucide React for icons, next/font for the type pair, Playwright for the production screenshot pass. The portfolio layer adds Zustand for toasts and Framer Motion for case study reveals, but the system layer stays narrow.
What is public
The whole system, live in production
The codebase is the source of truth: tokens, components, doc primitives, writing. The doc site and the portfolio are the same code, deployed separately, sharing the same primitives.