Every generator on your team — Copilot, Cursor, Claude Code — will invent its own buttons, colors, and spacing unless you condition it. Instruction files with named primitives, token rules, and one exemplar component turn "plausible React" into "our React," and they work on every PR from every teammate from now on.
Tier: Core · Workflow area: Component development · Tools: GitHub Copilot instructions, Cursor rules, Claude Code (CLAUDE.md)
Uninstructed models emit statistically average code: raw <button> elements, hex colors, margin: 12px, ad-hoc useState forms. Your design system is, by definition, not statistically average. This playbook builds the instruction layer once so on-system output becomes the default, not a per-prompt negotiation.
<Button> not <button>", "that's not a token" — each repeated comment is a missing instruction line.Mine your review history for the rules that matter. Instructions should encode real failure modes, not aspirations. Pull the evidence:
gh pr list --state merged --limit 50 --json number \
--jq '.[].number' | head -20 > /tmp/prs.txt
gh api repos/{owner}/{repo}/pulls/PR_NUMBER/comments \
--jq '.[].body' | grep -iE 'token|primitive|button|spacing|a11y'
Cluster the comments. The top five clusters are your first five rules.
Write the core instruction file. One file, three sections: what to use, what is banned, where the exemplars live. For Claude Code this is CLAUDE.md at the repo root:
## UI generation rules
- Components come from @acme/ui ONLY. Never write raw <button>, <input>,
<select>, <dialog>. Import: Button, TextField, Select, Modal, Stack,
Text, Icon. If a primitive is missing, STOP and ask — do not hand-roll.
- Styling: Tailwind with our preset only. Banned: arbitrary values
(w-[347px]), hex/rgb colors, inline style={}. Spacing uses the 4px
scale via Stack gap or p-* / m-* utilities.
- Color and type come from tokens: text-fg-*, bg-surface-*, text-heading-*.
- Every interactive element: visible focus ring (our Button handles it),
accessible name, keyboard operability.
- Forms: react-hook-form + zodResolver. See exemplar below.
- Exemplar component (canonical shape, copy its patterns):
src/features/settings/ProfileForm.tsx
- Exemplar list screen: src/features/orders/OrdersTable.tsx
Mirror it for every tool your team actually uses. Same content, three homes — do not let tools drift apart:
mkdir -p .github .cursor/rules
cp docs/ui-rules.md .github/copilot-instructions.md
cp docs/ui-rules.md .cursor/rules/ui.mdc
For Cursor, add frontmatter to .cursor/rules/ui.mdc so it auto-attaches to UI code:
---
description: Design-system rules for all UI code
globs: ["src/**/*.tsx", "src/**/*.vue"]
alwaysApply: true
---
Add one or two exemplar components — few-shot beats rules. Models imitate concrete code far better than they follow abstract rules. Pick your best-reviewed component of each archetype (form, table/list, detail card), make sure it is genuinely canonical, and reference it by path in the instructions. If a real exemplar doesn't exist, write one now; it pays for itself within a week.
Test the conditioning with an A/B generation. Same prompt, instructions off (scratch clone) vs. on:
Create a NotificationPreferences component: email/push/SMS toggles per
category (billing, security, product updates), save button with pending
state, server-error banner.
A good conditioned output imports Switch, Stack, Button, Banner from @acme/ui, uses token classes only, and follows the exemplar's form pattern. If it still hand-rolls a toggle, your instructions are ambiguous — name the missing primitive explicitly and rerun.
Enforce with a lint gate so drift can't merge. Instructions steer; CI enforces:
npx eslint --rulesdir tools/eslint src/ \
--rule '{"acme/no-raw-interactive-elements": "error"}'
Even a lightweight grep gate in CI catches the worst regressions:
grep -rn --include='*.tsx' -E '<button|style=\{\{|#[0-9a-fA-F]{6}' src/ \
&& echo 'Off-system UI found' && exit 1 || true
Assign ownership and a change ritual. Instruction files are code: PR-reviewed, owned by the design-system team (add them to CODEOWNERS), updated whenever a review comment repeats twice.
Instruction files are read by every model run but written once — a single vague line ("follow our design system") produces vague output on every generation across the whole team. Name the primitives, name the banned patterns, point at exemplars by path.
<script setup lang="ts">, composables over mixins, defineModel for v-model) and point exemplars at a canonical .vue component; glob src/**/*.vue.mat-* usage if your system wraps it; exemplar is a canonical standalone component with its spec file.<AppText>, ThemeData extensions), ban StyleSheet.create with raw values or hardcoded Color(0xFF...), and add platform rules: safe-area handling, touch targets ≥ 44pt.CLAUDE.md, .github/copilot-instructions.md, .cursor/rules/ generated from one source or diff-checked in CI.One well-mined instruction file with named primitives, explicit bans, and a canonical exemplar converts every AI tool on the team from a design-system liability into a design-system multiplier — and CI, not vigilance, keeps it that way.