---
name: design-system
description: >
  Build a new design system (tokens, variants, theming). Use when
  "create a design system" or "component library from scratch". Drifted
  existing system → housekeep-design. Plan-only unification →
  plan-uiux-unification.
license: MIT
---

# Design System Skill

**Degree of freedom: MIXED.** Token taxonomy and variants `[HIGH freedom]`;
existing `ui/` / token inventory `[LOW freedom — run exactly]`.

Build scalable, maintainable design systems with consistent tokens, variants, and documentation.

## How to reason

1. **Inventory** — `components/ui`, tokens, `cva`, shadcn/Radix
2. **Tokenize** — semantic tokens, not raw hex in components
3. **Variant** — `cva` covers real states (default/destructive/disabled)
4. **Document** — JSDoc, keyboard/focus, dark mode

## Worked example

> **Inventory:** shadcn `Button` exists; colors are raw `#3B82F6` in three pages; no `--primary`.
> **Tokenize:** `--primary` / `--primary-foreground` in `globals.css`; Tailwind maps `hsl(var(--primary))`.
> **Variant:** extend existing `buttonVariants` — do not add `components/Button2.tsx`.
> **Document:** JSDoc lists variants/sizes; focus ring + dark `.dark` overrides.

## Self-critique before reporting

- **Did not fork** — existing primitives were extended, not recreated
- **Semantic tokens** — components reference tokens, not primitives or hex
- **A11y + dark** — focus, disabled, and dark variants exist
- **Right owner** — drifted existing system → `housekeep-design`; plan-only unification → `plan-uiux-unification`

## CRITICAL: Check Existing First  [LOW freedom — run exactly]

**Before creating ANY design system components, verify:**

1. **Check for existing design system:**
```bash
ls -la src/components/ui/
cat package.json | grep -i "shadcn\|radix\|headless"
cat components.json 2>/dev/null # shadcn config
```

2. **Check for existing tokens:**
```bash
cat tailwind.config.* | head -100
cat src/styles/globals.css | head -50
rg "var\(--" --type css | head -20
```

3. **Check for existing patterns:**
```bash
rg "cva\(|variants:" --type ts --type tsx | head -10
rg "cn\(|clsx\(|twMerge" --type tsx | head -5
```

**Why:** Don't recreate existing primitives. Extend and enhance what exists.

## Design Tokens  [HIGH freedom]

### CSS Custom Properties
```css
/* globals.css */
:root {
 /* Colors - Semantic */
 --background: 0 0% 100%;
 --foreground: 222.2 84% 4.9%;
 --card: 0 0% 100%;
 --card-foreground: 222.2 84% 4.9%;
 --popover: 0 0% 100%;
 --popover-foreground: 222.2 84% 4.9%;
 --primary: 221.2 83.2% 53.3%;
 --primary-foreground: 210 40% 98%;
 --secondary: 210 40% 96.1%;
 --secondary-foreground: 222.2 47.4% 11.2%;
 --muted: 210 40% 96.1%;
 --muted-foreground: 215.4 16.3% 46.9%;
 --accent: 210 40% 96.1%;
 --accent-foreground: 222.2 47.4% 11.2%;
 --destructive: 0 84.2% 60.2%;
 --destructive-foreground: 210 40% 98%;
 --border: 214.3 31.8% 91.4%;
 --input: 214.3 31.8% 91.4%;
 --ring: 221.2 83.2% 53.3%;

 /* Spacing */
 --spacing-xs: 0.25rem;
 --spacing-sm: 0.5rem;
 --spacing-md: 1rem;
 --spacing-lg: 1.5rem;
 --spacing-xl: 2rem;

 /* Border Radius */
 --radius: 0.5rem;
 --radius-sm: calc(var(--radius) - 4px);
 --radius-lg: calc(var(--radius) + 4px);

 /* Shadows */
 --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
 --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
 --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
 --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);

 /* Animation */
 --duration-fast: 150ms;
 --duration-normal: 200ms;
 --duration-slow: 300ms;
 --ease-default: cubic-bezier(0.4, 0, 0.2, 1);
 --ease-in: cubic-bezier(0.4, 0, 1, 1);
 --ease-out: cubic-bezier(0, 0, 0.2, 1);
 --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
}

.dark {
 --background: 222.2 84% 4.9%;
 --foreground: 210 40% 98%;
 /* ... dark mode overrides */
}
```

### Tailwind Config Integration
```ts
// tailwind.config.ts
import type { Config } from 'tailwindcss'

const config: Config = {
 content: ['./src/**/*.{ts,tsx}'],
 darkMode: 'class',
 theme: {
 extend: {
 colors: {
 border: 'hsl(var(--border))',
 input: 'hsl(var(--input))',
 ring: 'hsl(var(--ring))',
 background: 'hsl(var(--background))',
 foreground: 'hsl(var(--foreground))',
 primary: {
 DEFAULT: 'hsl(var(--primary))',
 foreground: 'hsl(var(--primary-foreground))',
 },
 secondary: {
 DEFAULT: 'hsl(var(--secondary))',
 foreground: 'hsl(var(--secondary-foreground))',
 },
 destructive: {
 DEFAULT: 'hsl(var(--destructive))',
 foreground: 'hsl(var(--destructive-foreground))',
 },
 muted: {
 DEFAULT: 'hsl(var(--muted))',
 foreground: 'hsl(var(--muted-foreground))',
 },
 accent: {
 DEFAULT: 'hsl(var(--accent))',
 foreground: 'hsl(var(--accent-foreground))',
 },
 },
 borderRadius: {
 lg: 'var(--radius-lg)',
 md: 'var(--radius)',
 sm: 'var(--radius-sm)',
 },
 boxShadow: {
 sm: 'var(--shadow-sm)',
 DEFAULT: 'var(--shadow)',
 md: 'var(--shadow-md)',
 lg: 'var(--shadow-lg)',
 },
 transitionDuration: {
 fast: 'var(--duration-fast)',
 normal: 'var(--duration-normal)',
 slow: 'var(--duration-slow)',
 },
 },
 },
}

export default config
```

## Component Variants with CVA

```tsx
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
 return twMerge(clsx(inputs))
}
```

```tsx
// components/ui/button.tsx
import { cva, type VariantProps } from 'class-variance-authority'
import { forwardRef } from 'react'
import { cn } from '@/lib/utils'

const buttonVariants = cva(
 // Base styles
 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
 {
 variants: {
 variant: {
 default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
 destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
 outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
 secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
 ghost: 'hover:bg-accent hover:text-accent-foreground',
 link: 'text-primary underline-offset-4 hover:underline',
 },
 size: {
 default: 'h-9 px-4 py-2',
 sm: 'h-8 rounded-md px-3 text-xs',
 lg: 'h-10 rounded-md px-8',
 icon: 'h-9 w-9',
 },
 },
 defaultVariants: {
 variant: 'default',
 size: 'default',
 },
 }
)

export interface ButtonProps
 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
 VariantProps<typeof buttonVariants> {
 asChild?: boolean
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
 ({ className, variant, size, asChild = false, ...props }, ref) => {
 const Comp = asChild ? Slot : 'button'
 return (
 <Comp
 className={cn(buttonVariants({ variant, size, className }))}
 ref={ref}
 {...props}
 />
 )
 }
)
Button.displayName = 'Button'

export { Button, buttonVariants }
```

## Compound Components Pattern

```tsx
// components/ui/card.tsx
import { cn } from '@/lib/utils'
import { forwardRef } from 'react'

const Card = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
 ({ className, ...props }, ref) => (
 <div
 ref={ref}
 className={cn(
 'rounded-xl border bg-card text-card-foreground shadow',
 className
 )}
 {...props}
 />
 )
)
Card.displayName = 'Card'

const CardHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
 ({ className, ...props }, ref) => (
 <div
 ref={ref}
 className={cn('flex flex-col space-y-1.5 p-6', className)}
 {...props}
 />
 )
)
CardHeader.displayName = 'CardHeader'

const CardTitle = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
 ({ className, ...props }, ref) => (
 <h3
 ref={ref}
 className={cn('font-semibold leading-none tracking-tight', className)}
 {...props}
 />
 )
)
CardTitle.displayName = 'CardTitle'

const CardDescription = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
 ({ className, ...props }, ref) => (
 <p
 ref={ref}
 className={cn('text-sm text-muted-foreground', className)}
 {...props}
 />
 )
)
CardDescription.displayName = 'CardDescription'

const CardContent = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
 ({ className, ...props }, ref) => (
 <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
 )
)
CardContent.displayName = 'CardContent'

const CardFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
 ({ className, ...props }, ref) => (
 <div
 ref={ref}
 className={cn('flex items-center p-6 pt-0', className)}
 {...props}
 />
 )
)
CardFooter.displayName = 'CardFooter'

export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }
```

## Accessible Components with Radix

```tsx
// components/ui/dialog.tsx
'use client'

import * as DialogPrimitive from '@radix-ui/react-dialog'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'

const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close

const DialogOverlay = forwardRef<
 React.ElementRef<typeof DialogPrimitive.Overlay>,
 React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
 <DialogPrimitive.Overlay
 ref={ref}
 className={cn(
 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
 className
 )}
 {...props}
 />
))

const DialogContent = forwardRef<
 React.ElementRef<typeof DialogPrimitive.Content>,
 React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
 <DialogPortal>
 <DialogOverlay />
 <DialogPrimitive.Content
 ref={ref}
 className={cn(
 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
 className
 )}
 {...props}
 >
 {children}
 <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
 <X className="h-4 w-4" />
 <span className="sr-only">Close</span>
 </DialogPrimitive.Close>
 </DialogPrimitive.Content>
 </DialogPortal>
))

export { Dialog, DialogTrigger, DialogContent, DialogClose }
```

## Component Documentation Pattern

```tsx
// components/ui/button.stories.tsx (or docs)
/**
 * Button Component
 *
 * A versatile button component with multiple variants and sizes.
 *
 * @example
 * ```tsx
 * <Button variant="default" size="md">Click me</Button>
 * <Button variant="outline" size="sm">Small</Button>
 * <Button variant="destructive" disabled>Disabled</Button>
 * ```
 *
 * ## Variants
 * - `default` - Primary action button
 * - `secondary` - Secondary actions
 * - `destructive` - Dangerous actions (delete, remove)
 * - `outline` - Less prominent actions
 * - `ghost` - Minimal visual weight
 * - `link` - Styled as a link
 *
 * ## Sizes
 * - `sm` - 32px height
 * - `default` - 36px height
 * - `lg` - 40px height
 * - `icon` - 36x36px square for icon-only buttons
 *
 * ## Accessibility
 * - Uses native `<button>` element
 * - Supports `disabled` attribute
 * - Focus ring visible on keyboard navigation
 * - Works with `asChild` prop for custom elements
 */
```

## File Structure

```
src/
├── components/
│ └── ui/
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── select.tsx
│ ├── textarea.tsx
│ ├── toast.tsx
│ └── index.ts # Barrel export
├── lib/
│ └── utils.ts # cn() helper
├── styles/
│ └── globals.css # CSS tokens
└── tailwind.config.ts # Theme config
```

## Validation  [LOW freedom — do not skip]

After creating design system components:

1. **Consistency** → All components use same tokens
2. **Variants** → Cover all needed use cases
3. **Accessibility** → Keyboard nav, ARIA, focus states
4. **Dark mode** → All components work in dark mode
5. **Responsive** → Mobile-friendly by default
6. **Documentation** → JSDoc comments, usage examples
7. **Type safety** → Full TypeScript support with VariantProps
