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
Install ux-skill
From the workspace root.
# pnpm workspace $ pnpm add -DW uxskill # or via npx (no install) $ npx uxskill@alpha --version
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
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/**/*'] };
Add the design-lint task to turbo.json
Every app inherits it.
{
"tasks": {
"design-lint": { "dependsOn": ["^build"], "outputs": [] }
}
}
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
systemblock tells the AI tool what style, palette, type, and motion to generate within. - The
tokensblock tells the AI tool which CSS variables exist and are safe to reference. - The
forbiddenblock tells the linter what to flag — with named rule IDs that map to the 35-fingerprint taxonomy. - The
lint.scopeblock 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/webusesbg-brand;apps/adminusesbg-purple-500;apps/mobileuses 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-displayandhero-text-arbitrary-90pxrules 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-defaultandcubic-bezier-material-onlyrules catch the first two. - Inline style drift. AI-generated patches sneak
style="margin-top: 12px"into a Blade or JSX file. Theinline-style-attributerule catches it. - Token escape. A new color shows up nowhere in
tokens.css. Thetailwind-color-named-vaguerule 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.
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