ux
Blog · Monorepo · 2026-05-28

Monorepo design system with AI coding — ux-skill's tokens.css output.

A design system in a monorepo lives in one package and is consumed by everyone else. Turborepo and Nx make the wiring easy; what breaks is when AI-generated code in apps/web, apps/admin, and apps/mobile all drift from the shared system independently. ux-skill generates a tokens.css plus a manifest.json that lives in the shared package and gets enforced by a linter in every consumer app.

The monorepo design-system problem

A typical Turborepo or Nx setup has three or more app packages and one design-system package. The design system exports tokens (colors, spacing, type), components (Button, Card, Input), and primitives (CSS variables, Tailwind preset). The apps import them and ship.

That works fine for human-written code. With AI-generated code, the dynamic changes. Every apps/* package becomes a place where a Cursor agent, Claude Code skill, or Cascade run can produce drift — Inter at 90px in apps/admin, indigo gradient in apps/web, three equal cards in apps/mobile — even when packages/design-system defines none of those.

The shared package can't fix this alone because the design system is what to use, not what not to use. The not-to-use list is the 120 anti-pattern rules. Those rules need to run in every package that ships AI-generated code.

What ux-skill generates

Run ux recommend in the design-system package. ux-skill writes two files to the package root:

1. tokens.css

A flat CSS file with custom properties for every system value. Drops into the shared package as the source of truth. Apps import once, consume via var(--token-name) or through the Tailwind preset.

/* packages/design-system/tokens.css */
:root {
  /* Color · brand and semantic */
  --brand: #ec4899;
  --brand-active: #be185d;
  --canvas: #07080a;
  --surface: #0d0f12;
  --ink: #f6f7f9;
  --body: #c7ccd3;
  --muted: #8a8f96;
  --success: #34d399;
  --danger: #f472b6;

  /* Typography · type ramp */
  --font-display: 'Cormorant Garamond', serif;
  --font-body: 'Inter', sans-serif;
  --font-mono: 'JetBrains Mono', monospace;
  --text-display-xl: clamp(40px, 5.5vw, 64px);
  --text-h2: clamp(28px, 3.2vw, 36px);
  --text-body: 17px;
  --text-caption: 13px;

  /* Spacing scale */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;

  /* Radius · pill, card, panel */
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-pill: 999px;

  /* Motion · duration and ease */
  --ease-out-quart: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --duration-fast: 180ms;
  --duration-med: 260ms;
  --duration-slow: 420ms;

  /* Z-scale · stacking tokens */
  --z-base: 0;
  --z-dropdown: 10;
  --z-sticky: 20;
  --z-modal: 50;
  --z-toast: 60;
}

2. manifest.json

A structured description of the system. ux-skill's linter reads this in every consumer package and uses it to detect drift — "this file imports tokens.css but uses a raw bg-purple-500 Tailwind class that isn't mapped to any semantic token."

// packages/design-system/manifest.json
{
  "name": "acme-design-system",
  "version": "1.0.0",
  "system": {
    "style": "editorial-modern",
    "palette": "warm-cream-monochrome",
    "type_pairing": "cormorant-inter",
    "motion_preset": "restrained-cascade",
    "brand_spec": "linear-inspired"
  },
  "tokens": {
    "colors": ["brand", "canvas", "surface", "ink", "body", "muted", "success", "danger"],
    "type_scale": ["display-xl", "h2", "body", "caption"],
    "spacing": ["1", "2", "3", "4", "6", "8"],
    "radius": ["sm", "md", "lg", "pill"]
  },
  "forbidden": {
    "colors": ["raw-tailwind-named-shades", "purple-to-blue-gradient"],
    "type": ["inter-as-display", "arbitrary-hero-px"],
    "layout": ["three-equal-card-grid", "centered-everything-hero"],
    "motion": ["timing-300ms-default", "cubic-bezier-material-only"]
  },
  "lint": {
    "threshold": "high",
    "scope": ["apps/*/src/**/*.{tsx,jsx,vue,blade.php,astro}"]
  }
}

The manifest is the contract. Apps consume the tokens; the linter enforces the contract via the manifest.

Monorepo layout

A working layout with shared tokens and per-app lint:

monorepo/
├── package.json
├── turbo.json
├── packages/
│   └── design-system/
│       ├── tokens.css              # ux-skill generates
│       ├── manifest.json           # ux-skill generates
│       ├── tailwind-preset.js      # ux-skill generates
│       ├── package.json
│       └── src/
│           ├── Button.tsx
│           ├── Card.tsx
│           └── Input.tsx
├── apps/
│   ├── web/
│   │   ├── package.json
│   │   ├── tailwind.config.js      # extends preset
│   │   └── src/
│   ├── admin/
│   │   ├── package.json
│   │   ├── tailwind.config.js
│   │   └── src/
│   └── mobile/
│       ├── package.json
│       └── src/
└── .github/workflows/
    └── design-lint.yml             # runs ux lint on every PR

Wiring tailwind.config.js

Each app's tailwind.config.js extends the preset shipped from packages/design-system. The preset is generated by ux-skill from the same manifest.

// apps/web/tailwind.config.js
import preset from 'acme-design-system/tailwind-preset';

export default {
  presets: [preset],
  content: ['./src/**/*.{tsx,jsx,html}'],
};

Now apps/web, apps/admin, and apps/mobile share the palette, the type ramp, the radius scale, and the motion tokens. AI-generated code that uses bg-brand or text-display-xl stays on system. AI-generated code that uses raw bg-purple-500 drifts — and gets caught by the linter.

The linter as the enforcement boundary

ux-skill's ux lint command reads packages/design-system/manifest.json and applies the 120 anti-pattern rules to every consumer app's source. Run it locally, in a pre-commit hook, or as a Turborepo task.

As a Turborepo task

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "design-lint": {
      "dependsOn": ["^build"],
      "inputs": ["src/**/*", "../../packages/design-system/manifest.json"],
      "outputs": []
    }
  }
}

As a package script

// apps/web/package.json
{
  "scripts": {
    "design-lint": "ux lint ./src --manifest ../../packages/design-system/manifest.json --threshold high"
  }
}

As a GitHub Action

# .github/workflows/design-lint.yml
name: design-lint

on:
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install uxskill
      - run: npx turbo run design-lint

Turbo runs the lint task in every app package in parallel; exits 1 if any high-severity fingerprint shows up in any app. One PR, every app, every fingerprint, one report.

Why this works in Nx too

Nx maps cleanly to the same shape. Replace turbo.json with nx.json + project.json per package. The lint task becomes an Nx executor:

// apps/web/project.json
{
  "name": "web",
  "targets": {
    "design-lint": {
      "executor": "nx:run-commands",
      "options": {
        "command": "ux lint ./apps/web/src --manifest ./packages/design-system/manifest.json --threshold high"
      },
      "inputs": ["{projectRoot}/src/**/*", "{workspaceRoot}/packages/design-system/manifest.json"],
      "outputs": []
    }
  }
}

Same idea — the design-system package is the source of truth for tokens and the manifest; every app gets a design-lint target that reads the same manifest.

End-to-end install in a fresh Turborepo

01

Install ux-skill

From the workspace root.

# pnpm workspace
$ pnpm add -DW uxskill
# or via npx (no install)
$ npx uxskill@alpha --version
02

Generate the design system into packages/design-system

Run discover then recommend inside the package directory.

$ cd packages/design-system
$ npx uxskill@alpha discover            # 10-field intake
$ npx uxskill@alpha recommend            # writes tokens.css, manifest.json, preset
03

Wire each app's tailwind.config.js to the shared preset

One-line import per app.

import preset from 'acme-design-system/tailwind-preset';
export default { presets: [preset], content: ['./src/**/*'] };
04

Add the design-lint task to turbo.json

Every app inherits it.

{
  "tasks": {
    "design-lint": { "dependsOn": ["^build"], "outputs": [] }
  }
}
05

Run it

From the workspace root.

$ npx turbo run design-lint
# lints apps/web, apps/admin, apps/mobile in parallel
# exits 1 if any high-sev fingerprint is found

Why a manifest beats a config file

The design-system package could ship a plain config — a list of allowed colors, allowed fonts, allowed radii. But a config alone doesn't carry the "why" or the "fix" for each rule. The manifest does both:

Manifest in. Tokens out. Lint exit code is the contract.

The shared design-system package becomes the spec; AI tools in every app respect it; the linter enforces it. Drift gets caught at PR time, not at "why does admin look different from web" review time.

What this prevents

Concretely, the failure modes a monorepo design system catches when ux-skill is wired in:

What it doesn't do

Honesty card

The manifest contract works when every app respects it.

If one app skips the lint task, the contract breaks. The fix is to make the lint task non-optional: put it in turbo.json as a dependency of build, or wire it into your branch protection rules. AI-generated drift is fast to produce and slow to clean up; the gate has to be at PR time.

Also: ux-skill is 14 stars and v2 is alpha. If you need a battle-tested token system in a monorepo today, Style Dictionary or Tokens Studio are mature options. ux-skill's edge is the linter — token systems without enforcement are documents nobody reads.

Related reading

Install in your monorepo

One token file. One manifest. Every app on system.

Generate tokens.css and manifest.json once in the shared package. Wire the lint task into Turborepo or Nx. Every consumer app stays on system.

$ pnpm add -DW uxskill
$ cd packages/design-system
$ npx uxskill@alpha recommend
— or via pip —
$ pip install uxskill
$ ux lint ./apps/web --manifest ./packages/design-system/manifest.json
— or as a Claude Code skill —
$ /plugin marketplace add Laith0003/ux-skill
$ /plugin install ux@ux-skill