Design System · 2026

Production System

A hand-rolled production design system in Next.js and Tailwind v4

Token architecture, scoped CSS, 20+ React doc primitives, zero MDX. One repo powers three production sites with a shared design language.

Role

Design Engineer & MaintainerTokens, primitives, deploy

Stack

Next.js 14 + Tailwind v4App Router, CSS variables, TypeScript

Surface

Three production sitesDesign, Engineer, Doc system

Status

Shipped & Versionedv1.0, evolving

  • 8

    Foundation layers

    Color, type, space, radius, motion, ...

  • 20+

    React doc primitives

    Live components, MDX-free

  • ≤87 kB

    First-load JS shared

    Static-rendered, no client hydration for prose

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

  • 1
    Contrast: every pair tokenized and tested at WCAG AA. The CSS variable declarations are the receipt.
  • 2
    Focus: :focus-visible with a 2px outline and 2px offset. Nooutline: none without a replacement.
  • 3
    Keyboard: every interactive primitive is a real button, link, or form control. No divs with onClick. No role="button" on a div.
  • 4
    Reduced motion: @media (prefers-reduced-motion: reduce) gates every keyframe and transition duration. Tested in Chromium and Safari.
  • 5
    ARIA: 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.