Guide

Writing Cursor Rules That Enforce Your Design System

Learn how to write Cursor Rules that enforce your design system, with specific patterns that generate brand-consistent, maintainable code.

FramingUI Team13 min read

Cursor Rules let you configure how AI Composer generates code. Write rules once, they apply to every AI-generated file. The promise is compelling: encode your team's conventions, architecture preferences, and design system constraints, then let AI follow them automatically.

In practice, most Cursor Rules stay too generic. They say "use our design system" without specifying what that means. They list component names without explaining token structures or composition patterns. AI reads the rules and still generates code that looks plausible but doesn't match your system.

The difference between rules that work and rules that don't is specificity. This guide covers how to write Cursor Rules that actually enforce design system constraints.

What Cursor Rules Can and Can't Do

Cursor Rules are instructions that prepend to every AI prompt. When you write code with Composer, Cursor automatically includes your rules file as context. The AI reads the rules before generating code.

This means Cursor Rules are prompt engineering at scale. Instead of manually typing "use design tokens, follow our naming conventions" in every chat, you write it once in .cursorrules and it applies everywhere.

What this enables:

Enforcing conventions automatically. "Always import design tokens from @/tokens." "Use TypeScript for all new files." "Prefer function components over class components." AI applies these rules without you repeating them.

Teaching custom APIs. If you have a custom component library, Cursor Rules can document the API: "Our <Button> accepts variants: primary, secondary, destructive." AI generates correct code instead of guessing.

Preventing common mistakes. "Never use inline styles." "Don't import from ../../../. Use path aliases." AI avoids pitfalls you've encountered before.

What Cursor Rules cannot do:

Runtime validation. Rules are prompt context, not linters. If AI generates code that violates a rule, there's no automatic error. You catch it in review or testing.

Type enforcement. Rules don't replace TypeScript. If you want to prevent <Button variant="invalid">, that's a TypeScript constraint, not a Cursor Rule.

Multi-file coordination. Rules apply to the current generation context. They don't track state across sessions or ensure consistency between files created weeks apart.

The power is in teaching AI your system's conventions. The limitation is that enforcement is probabilistic, not guaranteed.

Generic Rules vs. Specific Rules

Most Cursor Rules files look like this:

# .cursorrules (generic version)

- Use our design system
- Follow component patterns
- Use semantic naming
- Write clean, maintainable code
- Follow accessibility best practices

These rules feel correct but are too vague to change AI behavior. Every instruction is an unmeasurable platitude. "Use clean code" means nothing to AI—it doesn't have a definition of "clean."

Contrast with specific rules:

# .cursorrules (specific version)

## Design Tokens
- Import tokens from @/design-tokens
- Use token references, never hardcoded values
- Valid color tokens: color.action.{primary|secondary|destructive}, color.surface.{background|card|elevated}, color.text.{primary|secondary|tertiary}
- Spacing tokens follow 4px scale: spacing.{1|2|3|4|6|8|12|16|24|32}
- Example: `backgroundColor: tokens.color.action.primary` not `backgroundColor: "#3b82f6"`

## Component Library
- Import components from @/components/ui
- Button variants: default, destructive, outline, secondary, ghost, link
- Button sizes: default, sm, lg, icon
- Card composition: <Card><CardHeader><CardTitle/><CardDescription/></CardHeader><CardContent/><CardFooter/></Card>
- Never invent new component variants. If needed, ask for guidance.

## File Structure
- Place new components in src/components/
- Co-locate tests: ComponentName.tsx + ComponentName.test.tsx
- Use path alias @/ for src/, never relative imports like ../../../

## TypeScript
- Use `interface` for component props, `type` for unions
- Export types alongside components
- No `any` types. Use `unknown` if type is truly dynamic.

## Forbidden Patterns
- No inline styles. Use className with Tailwind or style prop with token references.
- No `!important` in CSS
- No `console.log` in production code. Use proper logging library.

The second version is actionable. AI can read "valid color tokens: color.action.{primary|secondary|destructive}" and know exactly what's allowed. It can read "Button variants: default, destructive, outline" and avoid hallucinating variant="super-primary".

The rule: if AI can't generate code directly from the instruction, the instruction is too vague.

Documenting Token Structures in Rules

Design tokens are the foundation of AI-generated design system consistency. Your Cursor Rules should make token structure completely explicit.

Bad token documentation:

- Use design tokens

AI doesn't know what tokens exist or how to reference them.

Good token documentation:

## Design Tokens (import from @/design-tokens)

### Color Tokens
Semantic color roles organized by intent:
- Action colors: color.action.{primary|secondary|destructive}.{default|hover|active|disabled}
- Surface colors: color.surface.{background|card|elevated|overlay}
- Text colors: color.text.{primary|secondary|tertiary|inverse|disabled}
- Border colors: color.border.{default|hover|focus}

Usage: `style={{ backgroundColor: tokens.color.action.primary.default }}`
CSS variables: `var(--color-action-primary-default)`

### Spacing Tokens
Follow 4px base unit:
- spacing.1 = 4px
- spacing.2 = 8px
- spacing.3 = 12px
- spacing.4 = 16px
- spacing.6 = 24px
- spacing.8 = 32px
- spacing.12 = 48px
- spacing.16 = 64px

Usage: `style={{ padding: tokens.spacing.4 }}`
For Tailwind: Use spacing scale (p-4, m-6, gap-8)

### Typography Tokens
Predefined text styles:
- typography.hero: 48px/1.1, bold
- typography.h1: 36px/1.2, bold
- typography.h2: 30px/1.3, semibold
- typography.body: 16px/1.5, regular
- typography.label: 14px/1.4, medium
- typography.caption: 12px/1.4, regular

Usage: `className="typography-body"` or spread typography.body object into style prop

Now AI knows:

  1. Tokens are organized by semantic role (action, surface, text)
  2. Each token has modifiers (hover, active, disabled)
  3. How to reference tokens in code
  4. Valid token names (can't invent color.action.super-primary)

This level of detail transforms "use design tokens" from vague guidance to actionable constraint.

Encoding Component APIs

Document every component API that AI might generate. This prevents hallucinated props and invalid compositions.

Component with clear API rules:

## Button Component (import from @/components/ui/Button)

Props:
- variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
- size?: "default" | "sm" | "lg" | "icon"
- disabled?: boolean
- children: React.ReactNode

Examples:
Primary CTA: <Button variant="default">Submit</Button>
Destructive action: <Button variant="destructive">Delete</Button>
Secondary action: <Button variant="outline">Cancel</Button>
Small button: <Button size="sm">Close</Button>

Do NOT:
- Invent variants like "super-primary" or "cta"
- Use size="xs" or size="xl" (not defined)
- Pass color prop (use variant instead)

Card component with composition rules:

## Card Component (import from @/components/ui/Card)

Composition:
<Card>
  <CardHeader>
    <CardTitle>Title text</CardTitle>
    <CardDescription>Optional description</CardDescription>
  </CardHeader>
  <CardContent>
    Main content goes here
  </CardContent>
  <CardFooter>
    Optional footer with actions
  </CardFooter>
</Card>

Rules:
- CardHeader is optional but recommended for titled cards
- CardContent is required
- CardFooter is optional
- Order matters: Header → Content → Footer
- Do not nest Cards without semantic purpose

Example for settings panel:
<Card>
  <CardHeader>
    <CardTitle>Account Settings</CardTitle>
  </CardHeader>
  <CardContent>
    <form>...</form>
  </CardContent>
  <CardFooter>
    <Button variant="default">Save</Button>
    <Button variant="outline">Cancel</Button>
  </CardFooter>
</Card>

When AI generates a card, it follows the documented pattern. No missing CardContent wrappers. No invented CardBody component. Composition rules prevent structural mistakes.

Architecture and File Structure Rules

AI should know where files go and how to organize code. This prevents inconsistent project structure.

## Project Structure

### Component Placement
- UI primitives: src/components/ui/ (Button, Input, Card, etc.)
- Feature components: src/components/features/ (LoginForm, ProductCard, CheckoutWizard)
- Layout components: src/components/layout/ (Header, Sidebar, Footer)
- Page components: src/app/ or src/pages/ (depending on router)

### File Naming
- Components: PascalCase (Button.tsx, CardHeader.tsx)
- Utilities: camelCase (formatDate.ts, apiClient.ts)
- Hooks: camelCase with "use" prefix (useAuth.ts, useLocalStorage.ts)
- Types: PascalCase (UserProfile.ts, ApiResponse.ts)

### Co-location
- Tests next to source: Button.tsx + Button.test.tsx
- Styles with components if needed: Button.tsx + Button.module.css
- Types can be in same file for small components, separate for shared types

### Import Aliases
- @/ points to src/
- @/components/ for components
- @/lib/ for utilities
- @/design-tokens for tokens
Never use relative imports like ../../../../components/Button

### File Organization Example
src/
  components/
    ui/
      Button.tsx
      Button.test.tsx
      Card.tsx
      Card.test.tsx
    features/
      LoginForm.tsx
      LoginForm.test.tsx
  design-tokens/
    index.ts
    colors.ts
    spacing.ts
  lib/
    utils.ts
    api.ts

Now when AI creates a new component, it knows:

  • Where to place the file
  • How to name it
  • What import alias to use
  • Whether to co-locate tests

Project structure stays consistent across AI-generated files.

TypeScript and Type Safety Rules

AI's default TypeScript usage is permissive. Rules can enforce stricter patterns.

## TypeScript Standards

### Component Props
- Use `interface` for props, `type` for unions
- Export prop types alongside component
- Mark optional props with `?`, don't use `undefined` unions

Good:
interface ButtonProps {
  variant?: "default" | "destructive"
  disabled?: boolean
  children: React.ReactNode
}

Bad:
type ButtonProps = {
  variant: "default" | "destructive" | undefined  // Use ? instead
  disabled: boolean  // Should be optional
}

### Event Handlers
- Use React event types, not generic Function
- Name handlers with "handle" prefix

Good:
interface FormProps {
  onSubmit: (data: FormData) => void
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}

function handleSubmit(data: FormData) { ... }

Bad:
interface FormProps {
  onSubmit: Function  // Too generic
}

const submit = (data: any) => { ... }  // any is forbidden

### Forbidden
- No `any` types. Use `unknown` if type is truly unknown, then narrow with type guards
- No `@ts-ignore` or `@ts-expect-error` without explanation comment
- No unused variables or imports
- No implicit `any` (check tsconfig.json has `noImplicitAny: true`)

### Generics
When creating reusable components, use constrained generics:

Good:
interface SelectProps<T extends string | number> {
  value: T
  options: Array<{ value: T; label: string }>
}

Bad:
interface SelectProps {
  value: any
  options: any[]
}

These rules push AI toward type-safe patterns instead of permissive shortcuts.

Accessibility and Best Practices

Encode non-functional requirements that AI often skips:

## Accessibility Requirements

### Semantic HTML
- Use semantic elements: <button>, <nav>, <main>, <article>, <aside>
- Don't use <div> with onClick. Use <button>.
- Use <label> with form inputs

### ARIA Labels
- Add aria-label to icon-only buttons: <Button aria-label="Close dialog"><X /></Button>
- Use aria-describedby for error messages
- Add role attributes when semantic HTML isn't sufficient

### Keyboard Navigation
- All interactive elements must be keyboard accessible
- Modals must trap focus
- Add visible focus indicators (don't remove outline without replacement)

### Color Contrast
- Text must meet WCAG AA standards (4.5:1 for body, 3:1 for large text)
- Don't rely on color alone to convey information
- Use color.text.primary on color.surface.background (verified contrast ratios)

### Form Validation
- Provide error messages with specific guidance
- Mark required fields clearly
- Don't rely on placeholder text for labels

Example of accessible form:
<form>
  <label htmlFor="email">Email address *</label>
  <Input
    id="email"
    type="email"
    required
    aria-describedby="email-error"
  />
  {error && (
    <p id="email-error" role="alert">
      {error}
    </p>
  )}
</form>

AI won't remember accessibility rules without explicit encoding. These instructions make accessible patterns the default.

Testing and Quality Rules

Specify testing expectations:

## Testing Standards

### Test Co-location
- Every component gets a test file: Button.tsx → Button.test.tsx
- Place test next to source, not in separate __tests__ folder

### What to Test
- Render without errors
- Props work correctly (variants, sizes, disabled state)
- User interactions (click handlers, form submission)
- Accessibility (keyboard navigation, ARIA labels)

### Test Structure
Use Vitest and Testing Library:

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Button } from './Button'

describe('Button', () => {
  it('renders with default variant', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByRole('button')).toBeInTheDocument()
  })

  it('calls onClick handler', async () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click me</Button>)
    await userEvent.click(screen.getByRole('button'))
    expect(handleClick).toHaveBeenCalledOnce()
  })
})

### Don't Test
- Implementation details (internal state, class names)
- Third-party library behavior

With these rules, AI generates tests alongside components automatically.

Example: Complete .cursorrules File

A production-ready Cursor Rules file combining all patterns:

# .cursorrules for [Your Project]

## Design System

### Design Tokens (import from @/design-tokens)
- Always use tokens, never hardcoded values
- Color tokens: color.action.{primary|secondary|destructive}.{default|hover|active|disabled}
- Spacing tokens: spacing.{1|2|3|4|6|8|12|16} (4px base unit)
- Typography: typography.{hero|h1|h2|body|label|caption}
- Import: `import { tokens } from '@/design-tokens'`
- Usage: `style={{ backgroundColor: tokens.color.action.primary.default }}`

### Component Library (@/components/ui)

Button:
- Variants: default, destructive, outline, secondary, ghost, link
- Sizes: default, sm, lg, icon
- Example: `<Button variant="default" size="sm">Submit</Button>`

Card:
- Composition: `<Card><CardHeader><CardTitle/></CardHeader><CardContent/></Card>`
- CardHeader optional, CardContent required, CardFooter optional

Input:
- Always pair with <Label>
- Use aria-describedby for errors
- Example: `<Label htmlFor="email">Email</Label><Input id="email" type="email" />`

## Architecture

### File Structure
- UI components: src/components/ui/
- Feature components: src/components/features/
- Use @/ alias, never ../../../
- Co-locate tests: Component.tsx + Component.test.tsx

### TypeScript
- Use `interface` for props, `type` for unions
- Export prop types
- No `any` — use `unknown` then type-narrow
- React event types: `React.ChangeEvent`, `React.MouseEvent`

## Standards

### Accessibility
- Semantic HTML: <button> not <div onClick>
- aria-label for icon-only buttons
- <label> for all inputs
- Focus indicators visible

### Testing
- Generate tests with components
- Test user interactions, not implementation
- Use Testing Library queries: getByRole, getByLabelText

### Forbidden
- Inline styles (use className or tokens)
- Hardcoded colors/spacing
- `console.log` (use logger)
- Inventing component variants not listed above

## Examples

Good:
<Button variant="destructive" size="sm">Delete</Button>

Bad:
<Button variant="super-primary" color="red">Delete</Button>

Good:
<Card>
  <CardHeader><CardTitle>Settings</CardTitle></CardHeader>
  <CardContent>...</CardContent>
</Card>

Bad:
<Card>
  <CardBody>...</CardBody>  // CardBody doesn't exist
</Card>

Save this as .cursorrules in your project root. Cursor loads it automatically.

Measuring Rules Effectiveness

How do you know if your Cursor Rules work? Track AI-generated code quality:

Token compliance rate:

# Count CSS variable usage vs hardcoded colors
grep -r "var(--color-" src/ | wc -l  # Good
grep -rE "#[0-9a-f]{6}" src/ | wc -l  # Bad

Aim for 95%+ of colors coming from tokens.

Component API correctness: Review AI-generated components for hallucinated props. If AI generates <Button variant="super-primary">, your rules aren't specific enough.

Architecture consistency: Check if new AI-generated files land in correct directories. If AI creates components/ButtonThing.tsx instead of components/ui/Button.tsx, file structure rules need clarification.

Test coverage: Do AI-generated components include tests? If not, testing rules aren't clear or prominent enough.

The goal is 90%+ of AI-generated code following conventions without manual correction.

Integrating with FramingUI

FramingUI projects can use pre-built Cursor Rules templates:

npx framingui init --cursor-rules

This generates .cursorrules with:

  • Complete token structure documentation
  • All FramingUI component APIs
  • Recommended architecture patterns
  • TypeScript standards
  • Accessibility requirements

You customize from there. The template provides the foundation; you add project-specific rules (API patterns, domain models, testing preferences).

With FramingUI's MCP server + Cursor Rules, AI has two layers of context:

  1. MCP server — runtime queryable design system contract
  2. Cursor Rules — prompt-level conventions and examples

The combination ensures AI-generated code matches your design system consistently.


Cursor Rules are prompt engineering for your entire codebase. Generic rules produce generic results. Specific rules—explicit token structures, documented component APIs, clear architecture patterns—produce code that matches your design system without manual cleanup. Write rules that AI can act on directly, and AI-generated code becomes indistinguishable from hand-written code that follows your conventions.

Ready to build with FramingUI?

Build consistent UI with AI-ready design tokens. No more hallucinated colors or spacing.

Try FramingUI
Share

Related Posts