uxskill
Star on GitHub
Language
English 简体中文 繁體中文 日本語 한국어 हिन्दी Bahasa Indonesia Tiếng Việt ไทย العربية Español Français Deutsch Português Русский Türkçe Italiano
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.

By the ux-skill team · 7 min read · MIT-licensed open source

## 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 152 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 152 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:

- The `system` block tells the AI tool what style, palette, type, and motion to generate within.
- The `tokens` block tells the AI tool which CSS variables exist and are safe to reference.
- The `forbidden` block tells the linter what to flag, with named rule IDs that map to the 35-fingerprint taxonomy.
- The `lint.scope` block tells the linter which files to scan in which app.

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:

- Color drift across apps. `apps/web` uses `bg-brand`; `apps/admin` uses `bg-purple-500`; `apps/mobile` uses a hardcoded hex. The lint exit code blocks the PR.
- Type drift. One app ships Inter at 90px; another ships text-7xl; another ships clamp(). The `inter-as-display` and `hero-text-arbitrary-90px` rules catch both.
- Motion drift. One app uses 300ms default; another uses Material's cubic-bezier; another uses the manifest's spring preset. The `timing-300ms-default` and `cubic-bezier-material-only` rules catch the first two.
- Inline style drift. AI-generated patches sneak `style="margin-top: 12px"` into a Blade or JSX file. The `inline-style-attribute` rule catches it.
- Token escape. A new color shows up nowhere in `tokens.css`. The `tailwind-color-named-vague` rule catches it.

## What it doesn't do

- Doesn't replace your component library. ux-skill writes tokens and the manifest; `packages/design-system/src/` still owns Button, Card, Input. Pair with shadcn/ui or HeroUI for the actual components.
- Doesn't sync to Figma. Not in v2 (planned for v2.2). Until then, paste tokens manually into Figma variables.
- Doesn't catch semantic drift. A button labeled "Click here" passes every regex. The linter is for visual and structural fingerprints, not microcopy. Pair with hallmark or a human reviewer for that.

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

- How the Python recommender produces tokens.css
- The 35 AI design fingerprints · full taxonomy
- Anti-AI-slop tools · honest ranking
- Cursor design plugin · same engine in a single-app context
- Compare ux-skill v2 against the field
- About ux-skill · the project and the maker
- FAQ · Turborepo, Nx, and monorepo integration
- Roadmap · multi-package linter improvements

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

## Related reads

- Python design system generator
- Figma vs ux-skill
- Dogfooding ux-skill
- Regex linter for AI coding