UI Design System
When to Use
Activate this skill when:
- Creating new UI components that must follow a design system
- Building page layouts with consistent spacing and structure
- Setting up or extending design tokens (colors, typography, spacing)
- Choosing colors, fonts, or spacing values for a project
- Reviewing UI code for design consistency and accessibility
- Integrating shadcn/ui components into existing layouts
Do NOT use this skill for:
- Backend API implementation (use )
- Component or hook testing (use )
- E2E browser testing (use )
- General React patterns unrelated to design system (use )
- Deployment or CI/CD (use )
Instructions
Step 0: Read Existing Design Tokens
Before generating any UI code, check the project for existing tokens:
- Read (or ) for custom theme extensions
- Read or for CSS custom properties
- Read if shadcn/ui is configured
If no design tokens exist, generate a starter set and ask the user to confirm before proceeding (see Edge Cases).
Design Tokens
Color Tokens
Define colors as CSS custom properties consumed by Tailwind. Never use hardcoded hex/rgb values in components.
CSS custom properties (HSL format for shadcn/ui compatibility):
css
/* globals.css */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222 47% 11%;
--primary: 221 83% 53%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222 47% 11%;
--muted: 210 40% 96%;
--muted-foreground: 215 16% 47%;
--accent: 210 40% 96%;
--accent-foreground: 222 47% 11%;
--destructive: 0 84% 60%;
--destructive-foreground: 210 40% 98%;
--border: 214 32% 91%;
--input: 214 32% 91%;
--ring: 221 83% 53%;
--radius: 0.5rem;
}
.dark {
--background: 222 47% 11%;
--foreground: 210 40% 98%;
--primary: 217 91% 60%;
--primary-foreground: 222 47% 11%;
--secondary: 217 33% 17%;
--secondary-foreground: 210 40% 98%;
--muted: 217 33% 17%;
--muted-foreground: 215 20% 65%;
--accent: 217 33% 17%;
--accent-foreground: 210 40% 98%;
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
--border: 217 33% 17%;
--input: 217 33% 17%;
--ring: 224 76% 48%;
}
}
Tailwind config mapping:
ts
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
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))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
},
},
},
} satisfies Config;
Color usage rules:
- Always use semantic token classes: , ,
- Never use raw Tailwind palette colors () in component code
- Every color must have a dark mode variant defined
- Use variants for text on colored backgrounds
Typography Scale
Define a typographic scale using Tailwind's font-size utilities:
| Token | Size | Line Height | Usage |
|---|
| 12px | 16px | Captions, helper text |
| 14px | 20px | Secondary text, labels |
| 16px | 24px | Body text (default) |
| 18px | 28px | Subheadings |
| 20px | 28px | Section headings |
| 24px | 32px | Page headings |
| 30px | 36px | Hero headings |
Typography rules:
- Set a base font in :
fontFamily: { sans: ["Inter", "system-ui", "sans-serif"] }
- Use (500) for headings and labels, (400) for body
- Use for headings and above
- Limit line length with (65ch) for readability
Spacing (8pt Grid)
All spacing values follow an 8pt base grid:
| Tailwind Class | Value | Use Case |
|---|
| / | 4px | Inline icon padding, tight gaps |
| / | 8px | Compact element spacing |
| / | 12px | Input padding, small card padding |
| / | 16px | Standard component padding |
| / | 24px | Card padding, section gaps |
| / | 32px | Section padding |
| / | 48px | Page section spacing |
| / | 64px | Major layout spacing |
Spacing rules:
- Use for flex/grid children instead of individual margins
- Prefer for vertical stacking of sibling elements
- Cards: padding with between internal elements
- Page sections: or vertical padding
- Never mix spacing systems (no )
Component Structure
Hierarchy: Container > Layout > Content
Every component follows a three-layer structure:
tsx
// Container: outer wrapper with spacing, background, border
<Card className="p-6">
{/* Layout: flex/grid arrangement */}
<div className="flex items-center gap-4">
{/* Content: actual UI elements */}
<Avatar src={user.avatar} alt={user.name} />
<div className="space-y-1">
<h3 className="text-sm font-medium">{user.name}</h3>
<p className="text-sm text-muted-foreground">{user.role}</p>
</div>
</div>
</Card>
Semantic HTML
Use the correct HTML element for every purpose:
| Element | Use For | Not |
|---|
| Clickable actions | |
| Navigation links | for links |
| Navigation regions | |
| Primary page content | |
| Self-contained content (card, post) | |
| Thematic grouping with heading | |
| Sidebar or tangential content | |
| Introductory content for a section | |
| Footer content for a section | |
| / | Lists of items | for each item |
shadcn/ui Primitives
Prefer shadcn/ui components over custom implementations:
| Need | Use | Not |
|---|
| Buttons | | Custom with styles |
| Modals | | Custom modal with portal |
| Dropdowns | | Custom dropdown |
| Cards | | Styled |
| Inputs | | Styled |
| Selects | | Native |
| Tooltips | | Custom tooltip |
| Tabs | | Custom tab component |
| Tables | | Plain |
| Alerts | | Custom banner div |
If shadcn/ui is not installed, fall back to plain Tailwind with equivalent patterns and consistent class ordering.
TypeScript Component Interfaces
Export props as TypeScript interfaces with JSDoc descriptions:
tsx
/** Props for the UserProfileCard component. */
interface UserProfileCardProps {
/** User data to display. */
user: User;
/** Called when the edit button is clicked. */
onEdit?: (userId: string) => void;
/** Visual variant of the card. */
variant?: "default" | "compact";
/** Additional CSS classes applied to the root element. */
className?: string;
}
export function UserProfileCard({
user,
onEdit,
variant = "default",
className,
}: UserProfileCardProps) {
// ...
}
Props rules:
- Name interface
- Include on every component for composition
- Use prop for visual variations, not separate components
- Default optional props in destructuring, not in interface
- Use union types for constrained string values:
Responsive Design
Breakpoints
Use Tailwind's mobile-first breakpoints:
| Prefix | Min Width | Target |
|---|
| (none) | 0px | Mobile (default) |
| 640px | Large phones / small tablets |
| 768px | Tablets |
| 1024px | Desktops |
| 1280px | Large desktops |
Responsive rules:
- Design mobile-first: base styles for mobile, then add breakpoint overrides
- Use
grid-cols-1 md:grid-cols-2 lg:grid-cols-3
for responsive grids
- Stack navigation vertically on mobile:
- Hide non-essential elements on mobile:
- Set max container width:
max-w-7xl mx-auto px-4 sm:px-6 lg:px-8
Responsive Layout Pattern
tsx
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map((item) => (
<ItemCard key={item.id} item={item} />
))}
</div>
</div>
Accessibility (WCAG 2.1 AA)
Contrast Ratios
- Normal text (< 18px or < 14px bold): minimum 4.5:1 contrast ratio
- Large text (>= 18px or >= 14px bold): minimum 3:1 contrast ratio
- UI components and graphical objects: minimum 3:1 contrast ratio
Interactive Element Requirements
Every interactive element must have:
- Visible label or aria-label:
<Button aria-label="Close dialog">X</Button>
- Focus indicator: Tailwind's utilities:
focus-visible:ring-2 focus-visible:ring-ring
- Keyboard access: Reachable via Tab, activatable via Enter/Space
- Disabled state: Both visual and or attribute
ARIA Patterns
- Icon-only buttons:
- Loading states: on the loading container
- Dynamic content updates: on the container
- Form errors: on the input, on the message
- Modals: , focus trap, Escape to close
- Navigation landmarks:
<nav aria-label="Main navigation">
Examples
User Profile Card
tsx
interface UserProfileCardProps {
user: { id: string; name: string; role: string; avatarUrl: string };
onEdit?: (userId: string) => void;
className?: string;
}
export function UserProfileCard({ user, onEdit, className }: UserProfileCardProps) {
return (
<Card className={cn("p-6", className)}>
<div className="flex items-center gap-4">
<Avatar className="h-12 w-12">
<AvatarImage src={user.avatarUrl} alt={user.name} />
<AvatarFallback>{user.name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="space-y-1">
<h3 className="text-sm font-medium leading-none">{user.name}</h3>
<p className="text-sm text-muted-foreground">{user.role}</p>
</div>
</div>
{onEdit && (
<div className="mt-4">
<Button
variant="outline"
size="sm"
onClick={() => onEdit(user.id)}
aria-label={`Edit ${user.name}'s profile`}
>
Edit Profile
</Button>
</div>
)}
</Card>
);
}
SaaS Dashboard Design Tokens Setup
Tailwind config extension:
ts
// tailwind.config.ts
import type { Config } from "tailwindcss";
export default {
theme: {
extend: {
colors: {
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))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
},
fontFamily: {
sans: ["Inter", "system-ui", "sans-serif"],
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
} satisfies Config;
Responsive Dashboard Layout
tsx
export function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-background">
<nav className="border-b border-border" aria-label="Main navigation">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center">
{/* nav content */}
</div>
</nav>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-[240px_1fr] gap-8">
<aside className="hidden lg:block" aria-label="Sidebar">
{/* sidebar content */}
</aside>
<main>{children}</main>
</div>
</div>
</div>
);
}
Edge Cases
-
No existing design tokens: Generate a starter token set (see
references/design-tokens-reference.md
) and present it to the user for confirmation before writing any component code. Ask: "No design tokens found. Here's a starter set — should I apply these?"
-
shadcn/ui not installed: Fall back to plain Tailwind with equivalent patterns. Use
instead of
, styled
instead of
. Maintain the same spacing and color token approach.
-
Component overlap: If a requested component duplicates an existing one, flag it: "A similar
component exists at
src/components/UserCard.tsx
. Should I extend it or create a separate component?"
-
Dark mode tokens missing: If
tokens exist but
variants are absent, generate matching dark variants before proceeding. Every semantic color token must have both light and dark values.
-
Custom brand colors: When the user provides specific brand hex values, convert them to HSL and integrate into the token system. Never use the hex values directly in components.
-
Inconsistent spacing in existing code: Flag the inconsistency, suggest the closest 8pt grid values, and ask whether to normalize existing components or only apply the grid to new code.
See
references/design-tokens-reference.md
for starter token sets, color palette guide, and typography scales.