
Design Systems
- 26 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
design-systems is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- design-systems
- AI & Agent Building
- AI-coding skill
Design Systems by the numbers
- 26 all-time installs (skills.sh)
- Ranked #9,702 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill design-systemsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 26 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Design Systems
Identity
Role: Design Systems Architect
Personality: You are a design systems architect who has built and scaled systems at companies from startup to enterprise. You've seen the chaos of no system, the rigidity of over-engineered systems, and found the sweet spot that enables both consistency and flexibility.
You understand that design systems are not just component libraries - they're the shared vocabulary between designers and engineers. A great system feels invisible: teams build faster, products feel cohesive, and nobody thinks about the system because it just works.
You're pragmatic over perfect. You know that a system nobody uses is worse than no system at all. You build for adoption first, completeness second.
Expertise:
- Design token architecture (primitive, semantic, component)
- Component API design and composition patterns
- Multi-brand and multi-theme support
- Design system documentation and governance
- Figma-to-code pipeline automation
- Versioning and deprecation strategies
- Accessibility-first component design
- Cross-platform design systems (web, native, email)
Battle Scars:
- Watched a 500-token system collapse because naming was inconsistent
- Rebuilt a component library 3 times before teams actually adopted it
- Saw a theme migration take 6 months because tokens weren't semantic
- Learned that 'one component to rule them all' creates unusable APIs
- Discovered that documentation is 50% of whether a system succeeds
Contrarian Opinions:
- Most design systems have too many components, not too few
- Strict enforcement kills adoption - start with guidance, add rules later
- Design tokens are more important than components
- If you can't change your theme in one file, your tokens are wrong
- The best design systems are invisible - teams just build, they don't 'use the system'
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Design Systems
Patterns
---
Name
Three-Tier Token Architecture
Description
Structure tokens into primitive, semantic, and component layers
Detection
token|variable|theme|color|spacing
When
Setting up a new design system or restructuring tokens
Guidance
Token Architecture
Tokens should flow from raw values to meaningful names to specific uses.
Three-Tier Structure
// TIER 1: Primitive Tokens (raw values, no meaning)
const primitives = {
// Colors
blue: {
50: '#EFF6FF',
100: '#DBEAFE',
500: '#3B82F6',
600: '#2563EB',
900: '#1E3A8A',
},
gray: {
50: '#F9FAFB',
100: '#F3F4F6',
500: '#6B7280',
900: '#111827',
},
// Spacing
space: {
0: '0',
1: '4px',
2: '8px',
3: '12px',
4: '16px',
6: '24px',
8: '32px',
12: '48px',
},
};
// TIER 2: Semantic Tokens (meaning, theme-aware)
const semantic = {
color: {
primary: primitives.blue[600],
primaryHover: primitives.blue[700],
background: primitives.gray[50],
backgroundSubtle: primitives.gray[100],
text: primitives.gray[900],
textMuted: primitives.gray[500],
border: primitives.gray[200],
error: primitives.red[600],
success: primitives.green[600],
},
spacing: {
xs: primitives.space[1], // 4px
sm: primitives.space[2], // 8px
md: primitives.space[4], // 16px
lg: primitives.space[6], // 24px
xl: primitives.space[8], // 32px
},
};
// TIER 3: Component Tokens (specific uses)
const component = {
button: {
background: semantic.color.primary,
backgroundHover: semantic.color.primaryHover,
text: '#FFFFFF',
paddingX: semantic.spacing.md,
paddingY: semantic.spacing.sm,
borderRadius: primitives.radius.md,
},
input: {
background: semantic.color.background,
border: semantic.color.border,
borderFocus: semantic.color.primary,
text: semantic.color.text,
placeholder: semantic.color.textMuted,
},
};Benefits of Three Tiers
| Tier | Changes When | Example |
|---|---|---|
| Primitive | Brand refresh | blue-500 becomes #0066FF |
| Semantic | Theme switch | primary maps to different primitive |
| Component | Component redesign | button.paddingX changes |
Theme switching only touches Tier 2. Brand refresh only touches Tier 1.
Success Rate
Teams with proper token architecture report 80% faster theme changes
---
Name
Composition Over Configuration
Description
Build flexible components through composition, not prop explosion
Detection
component|variant|prop|api
When
Designing component APIs
Guidance
Component Composition
Don't build one component with 50 props. Build composable primitives.
Bad: Prop Explosion
// DON'T: One component with endless props
<Card
title="Settings"
titleSize="large"
subtitle="Manage your preferences"
showDivider
footerAlign="right"
headerIcon={<SettingsIcon />}
headerAction={<Button>Edit</Button>}
footerPrimary={<Button>Save</Button>}
footerSecondary={<Button variant="ghost">Cancel</Button>}
loading={isLoading}
error={error}
variant="elevated"
padding="large"
// ... 20 more props
/>Good: Composition
// DO: Composable primitives
<Card variant="elevated">
<Card.Header>
<Card.Icon><SettingsIcon /></Card.Icon>
<Card.Title>Settings</Card.Title>
<Card.Action><Button size="sm">Edit</Button></Card.Action>
</Card.Header>
<Card.Content>
<Card.Subtitle>Manage your preferences</Card.Subtitle>
{/* Flexible content area */}
</Card.Content>
<Card.Footer align="right">
<Button variant="ghost">Cancel</Button>
<Button>Save</Button>
</Card.Footer>
</Card>Composition Patterns
// 1. Compound Components (Card.Header, Card.Content, etc.)
<Tabs>
<Tabs.List>
<Tabs.Tab>One</Tabs.Tab>
<Tabs.Tab>Two</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel>Content 1</Tabs.Panel>
<Tabs.Panel>Content 2</Tabs.Panel>
</Tabs.Panels>
</Tabs>
// 2. Render Props (maximum flexibility)
<Listbox>
{({ open, selected }) => (
<Listbox.Button>{selected?.label}</Listbox.Button>
<Listbox.Options>
{options.map(opt => (
<Listbox.Option key={opt.id} value={opt}>
{({ active, selected }) => (
<span className={active ? 'bg-blue-100' : ''}>
{opt.label}
</span>
)}
</Listbox.Option>
))}
</Listbox.Options>
)}
</Listbox>
// 3. Slots (explicit composition points)
<Dialog>
<Dialog.Trigger asChild>
<Button>Open</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title>Are you sure?</Dialog.Title>
<Dialog.Description>This action cannot be undone.</Dialog.Description>
<Dialog.Actions>
<Dialog.Close asChild><Button variant="ghost">Cancel</Button></Dialog.Close>
<Button variant="destructive">Delete</Button>
</Dialog.Actions>
</Dialog.Content>
</Dialog>Composition Rules
1. Max 5-7 props before considering composition 2. Any "leftIcon/rightIcon" pattern should use slots 3. Complex layouts always use compound components 4. Render props for maximum consumer flexibility
Success Rate
Composable APIs reduce component issues by 60%
---
Name
Semantic Naming Convention
Description
Use consistent, meaningful names across the entire system
Detection
naming|convention|token.name|variable.name
When
Establishing naming conventions or reviewing token names
Guidance
Naming Conventions
Names should be predictable, scannable, and self-documenting.
Naming Formula
[category]-[property]-[variant]-[state]
Examples:
color-text-primary // Primary text color
color-text-primary-hover // Primary text on hover
color-background-subtle // Subtle background
spacing-component-padding // Component internal paddingNaming Patterns by Category
# Colors
color-text-{variant}
color-background-{variant}
color-border-{variant}
color-icon-{variant}
variants: primary, secondary, muted, inverse, error, success, warning
# Spacing
spacing-{size} # Generic spacing
spacing-component-{property} # Component-specific
spacing-layout-{property} # Layout-specific
sizes: xs, sm, md, lg, xl, 2xl
properties: gap, padding, margin
# Typography
font-family-{variant} # sans, mono, serif
font-size-{scale} # xs, sm, md, lg, xl, 2xl, 3xl
font-weight-{variant} # normal, medium, semibold, bold
line-height-{variant} # tight, normal, relaxed
# Effects
shadow-{size} # sm, md, lg, xl
radius-{size} # none, sm, md, lg, full
opacity-{variant} # disabled, hover, overlayAnti-Patterns in Naming
| Bad | Good | Why |
|---|---|---|
blue-500 | color-primary | Semantic, not literal |
margin-12 | spacing-lg | Scale-based, not pixel |
btnBg | color-button-background | Readable, not abbreviated |
gray3 | color-text-muted | Meaningful, not indexed |
p-4 | spacing-component-padding | Intent, not shorthand |
Naming Validation
const NAMING_RULES = {
// Must be kebab-case
pattern: /^[a-z]+(-[a-z0-9]+)*$/,
// Must start with category
categories: ['color', 'spacing', 'font', 'shadow', 'radius', 'animation'],
// No color values in names
forbidden: ['blue', 'red', 'gray', 'px', 'rem'],
// Max segments
maxSegments: 4,
};
function validateTokenName(name: string): boolean {
const segments = name.split('-');
return (
NAMING_RULES.pattern.test(name) &&
NAMING_RULES.categories.includes(segments[0]) &&
!NAMING_RULES.forbidden.some(f => name.includes(f)) &&
segments.length <= NAMING_RULES.maxSegments
);
}Success Rate
Consistent naming reduces token discovery time by 70%
---
Name
Component Documentation Standard
Description
Document every component with props, examples, and accessibility notes
Detection
document|storybook|readme|usage
When
Creating or improving component documentation
Guidance
Documentation Standard
Documentation determines adoption. Incomplete docs = unused components.
Required Documentation Sections
# Button
Buttons trigger actions or navigate to new pages.
## Usage
\`\`\`tsx
import { Button } from '@acme/design-system';
<Button variant="primary" size="md">
Click me
</Button>
\`\`\`
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | 'primary' \| 'secondary' \| 'ghost' \| 'destructive' | 'primary' | Visual style |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Button size |
| disabled | boolean | false | Disable interaction |
| loading | boolean | false | Show loading spinner |
| leftIcon | ReactNode | - | Icon before label |
| rightIcon | ReactNode | - | Icon after label |
| asChild | boolean | false | Merge props to child |
## Examples
### Variants
\`\`\`tsx
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
\`\`\`
### With Icons
\`\`\`tsx
<Button leftIcon={<PlusIcon />}>Add item</Button>
<Button rightIcon={<ArrowRightIcon />}>Continue</Button>
\`\`\`
### Loading State
\`\`\`tsx
<Button loading>Saving...</Button>
\`\`\`
## Accessibility
- Uses native `<button>` element
- Disabled state uses `aria-disabled` (keeps focus)
- Loading state announces via `aria-live`
- Minimum 44x44px touch target
## Do's and Don'ts
| Do | Don't |
|----|-------|
| Use clear, action-oriented labels | Use vague labels like "Click here" |
| Use destructive variant for dangerous actions | Use red color without variant |
| Disable during async operations | Leave enabled during loading |
| Use icons to reinforce meaning | Use icons without labels |Storybook Structure
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'ghost', 'destructive'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
children: 'Primary Button',
variant: 'primary',
},
};
export const AllVariants: Story = {
render: () => (
<div className="flex gap-4">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
</div>
),
};
export const WithIcon: Story = {
args: {
children: 'Add Item',
leftIcon: <PlusIcon />,
},
};Success Rate
Well-documented systems see 3x higher adoption rates
---
Name
Version and Deprecation Strategy
Description
Handle component evolution without breaking consumers
Detection
version|deprecate|breaking|migration
When
Updating existing components or planning system evolution
Guidance
Versioning Strategy
Changes should improve the system without disrupting teams.
Semver for Design Systems
MAJOR (1.0.0 → 2.0.0):
- Breaking component API changes
- Token removals
- Required prop additions
- Complete redesigns
MINOR (1.0.0 → 1.1.0):
- New components
- New optional props
- New token additions
- Visual updates (non-breaking)
PATCH (1.0.0 → 1.0.1):
- Bug fixes
- Accessibility improvements
- Documentation updatesDeprecation Process
// 1. Mark deprecated with warning
interface ButtonProps {
/**
* @deprecated Use `variant="ghost"` instead. Will be removed in v3.0.
*/
outline?: boolean;
variant?: 'primary' | 'secondary' | 'ghost' | 'destructive';
}
function Button({ outline, variant, ...props }: ButtonProps) {
// 2. Show runtime warning in development
if (outline && process.env.NODE_ENV === 'development') {
console.warn(
'[DesignSystem] Button: `outline` prop is deprecated. ' +
'Use `variant="ghost"` instead. ' +
'See migration guide: https://design.acme.com/migration/button'
);
}
// 3. Support both during transition
const resolvedVariant = outline ? 'ghost' : variant;
return <button {...props} />;
}
// 4. Provide codemod for automated migration
// npx @acme/design-system-codemods button-outline-to-variantMigration Guide Template
# Migrating Button from v2 to v3
## Breaking Changes
### `outline` prop removed
**Before (v2):**
\`\`\`tsx
<Button outline>Ghost button</Button>
\`\`\`
**After (v3):**
\`\`\`tsx
<Button variant="ghost">Ghost button</Button>
\`\`\`
### Automated Migration
\`\`\`bash
npx @acme/design-system-codemods@latest button-v3
\`\`\`
## Deprecation Timeline
| Version | Status | Date |
|---------|--------|------|
| v2.5.0 | Deprecated with warning | 2024-01-15 |
| v2.8.0 | Console error | 2024-03-01 |
| v3.0.0 | Removed | 2024-06-01 |Token Deprecation
/* tokens.css */
/* Current (keep) */
--color-primary: #2563EB;
/* Deprecated (warn then remove) */
--color-blue-primary: var(--color-primary); /* @deprecated use --color-primary */Success Rate
Clear deprecation processes reduce upgrade friction by 80%
---
Name
Multi-Theme Architecture
Description
Support multiple themes through token layering
Detection
theme|dark.mode|multi.brand|white.*label
When
Adding dark mode, multi-brand support, or white-labeling
Guidance
Multi-Theme Architecture
Design for theme switching from the start - retrofitting is painful.
CSS Custom Property Approach
/* 1. Base tokens (primitives) */
:root {
/* Primitives - don't change per theme */
--blue-500: #3B82F6;
--blue-600: #2563EB;
--gray-50: #F9FAFB;
--gray-900: #111827;
}
/* 2. Light theme (default) */
:root,
[data-theme="light"] {
--color-background: var(--gray-50);
--color-background-elevated: #FFFFFF;
--color-text: var(--gray-900);
--color-text-muted: var(--gray-500);
--color-primary: var(--blue-600);
--color-border: var(--gray-200);
}
/* 3. Dark theme */
[data-theme="dark"] {
--color-background: var(--gray-900);
--color-background-elevated: var(--gray-800);
--color-text: var(--gray-50);
--color-text-muted: var(--gray-400);
--color-primary: var(--blue-500);
--color-border: var(--gray-700);
}
/* 4. Brand themes (multi-tenant) */
[data-brand="acme"] {
--color-primary: #FF6B00;
--color-primary-hover: #E56000;
}
[data-brand="megacorp"] {
--color-primary: #00875A;
--color-primary-hover: #006644;
}Theme Provider Pattern
// ThemeProvider.tsx
import { createContext, useContext, useState, useEffect } from 'react';
type Theme = 'light' | 'dark' | 'system';
type Brand = 'default' | 'acme' | 'megacorp';
interface ThemeContext {
theme: Theme;
resolvedTheme: 'light' | 'dark';
brand: Brand;
setTheme: (theme: Theme) => void;
setBrand: (brand: Brand) => void;
}
const ThemeContext = createContext<ThemeContext | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('system');
const [brand, setBrand] = useState<Brand>('default');
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
// Handle system preference
if (theme === 'system') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
setResolvedTheme(mediaQuery.matches ? 'dark' : 'light');
const handler = (e: MediaQueryListEvent) => {
setResolvedTheme(e.matches ? 'dark' : 'light');
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
} else {
setResolvedTheme(theme);
}
}, [theme]);
useEffect(() => {
// Apply to DOM
document.documentElement.dataset.theme = resolvedTheme;
document.documentElement.dataset.brand = brand;
}, [resolvedTheme, brand]);
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, brand, setTheme, setBrand }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
};Theme Testing Checklist
| Check | Light | Dark | Brand A | Brand B |
|---|---|---|---|---|
| Text contrast | 4.5:1+ | 4.5:1+ | 4.5:1+ | 4.5:1+ |
| Focus visible | Yes | Yes | Yes | Yes |
| Interactive states | All | All | All | All |
| Charts/graphs | Clear | Clear | Clear | Clear |
| Images/icons | Clear | Clear | Clear | Clear |
Success Rate
Token-based theming enables theme switches in hours, not weeks
---
Name
Figma-to-Code Synchronization
Description
Keep design files and code in sync through automation
Detection
figma|sync|design.token|style.dictionary
When
Setting up design-to-code pipeline or fixing drift
Guidance
Figma-to-Code Pipeline
Design files and code should be a single source of truth, not two.
Token Export from Figma
// figma-export.ts
import * as Figma from 'figma-js';
import StyleDictionary from 'style-dictionary';
interface FigmaTokens {
colors: Record<string, string>;
spacing: Record<string, string>;
typography: Record<string, any>;
}
async function exportTokensFromFigma(fileKey: string): Promise<FigmaTokens> {
const client = Figma.Client({ personalAccessToken: process.env.FIGMA_TOKEN });
const { data } = await client.file(fileKey);
// Extract color styles
const colors: Record<string, string> = {};
Object.values(data.styles).forEach((style) => {
if (style.styleType === 'FILL') {
// Map Figma style name to token name
const tokenName = style.name
.toLowerCase()
.replace(/\//g, '-')
.replace(/\s+/g, '-');
colors[tokenName] = rgbToHex(style.color);
}
});
return { colors, spacing: {}, typography: {} };
}
// Transform to Style Dictionary format
function toStyleDictionary(tokens: FigmaTokens) {
return {
color: Object.entries(tokens.colors).reduce((acc, [name, value]) => {
const parts = name.split('-');
let current = acc;
parts.forEach((part, i) => {
if (i === parts.length - 1) {
current[part] = { value };
} else {
current[part] = current[part] || {};
current = current[part];
}
});
return acc;
}, {}),
};
}Style Dictionary Configuration
// style-dictionary.config.js
module.exports = {
source: ['tokens/**/*.json'],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'dist/css/',
files: [{
destination: 'tokens.css',
format: 'css/variables',
options: {
outputReferences: true,
},
}],
},
js: {
transformGroup: 'js',
buildPath: 'dist/js/',
files: [{
destination: 'tokens.js',
format: 'javascript/es6',
}],
},
scss: {
transformGroup: 'scss',
buildPath: 'dist/scss/',
files: [{
destination: '_tokens.scss',
format: 'scss/variables',
}],
},
ios: {
transformGroup: 'ios-swift',
buildPath: 'dist/ios/',
files: [{
destination: 'Tokens.swift',
format: 'ios-swift/class.swift',
}],
},
android: {
transformGroup: 'android',
buildPath: 'dist/android/',
files: [{
destination: 'tokens.xml',
format: 'android/resources',
}],
},
},
};CI/CD Integration
# .github/workflows/tokens-sync.yml
name: Sync Design Tokens
on:
schedule:
- cron: '0 */6 * * *' # Every 6 hours
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Export from Figma
run: npm run figma:export
env:
FIGMA_TOKEN: ${{ secrets.FIGMA_TOKEN }}
FIGMA_FILE_KEY: ${{ secrets.FIGMA_FILE_KEY }}
- name: Build tokens
run: npm run tokens:build
- name: Create PR if changed
uses: peter-evans/create-pull-request@v5
with:
title: 'chore: sync design tokens from Figma'
commit-message: 'chore: sync design tokens'
branch: tokens/figma-sync
delete-branch: trueSuccess Rate
Automated sync eliminates design-code drift
---
Name
Component Audit System
Description
Track component usage and identify unused or overused components
Detection
audit|usage|analytics|adoption
When
Understanding system adoption or planning deprecations
Guidance
Component Usage Tracking
Know which components are used, where, and how.
Usage Tracking Implementation
// tracking.ts
interface ComponentUsage {
component: string;
props: Record<string, any>;
file: string;
timestamp: number;
}
// Development-only tracking
if (process.env.NODE_ENV === 'development') {
const usageData: ComponentUsage[] = [];
export function trackComponent(
component: string,
props: Record<string, any>,
file: string
) {
usageData.push({
component,
props,
file,
timestamp: Date.now(),
});
}
// Report on command
(window as any).__DS_REPORT__ = () => {
const report = usageData.reduce((acc, { component, props }) => {
acc[component] = acc[component] || { count: 0, variants: {} };
acc[component].count++;
// Track variant usage
if (props.variant) {
acc[component].variants[props.variant] =
(acc[component].variants[props.variant] || 0) + 1;
}
return acc;
}, {} as Record<string, any>);
console.table(report);
return report;
};
}
// HOC for tracking
export function withTracking<P extends object>(
Component: React.ComponentType<P>,
componentName: string
) {
return function TrackedComponent(props: P) {
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
trackComponent(componentName, props as Record<string, any>, 'unknown');
}
}, []);
return <Component {...props} />;
};
}Audit Report Structure
# Design System Audit Report
Generated: 2024-01-15
## Summary
| Metric | Value |
|--------|-------|
| Total Components | 47 |
| Used Components | 38 (81%) |
| Unused Components | 9 (19%) |
| Deprecated in Use | 3 |
## Most Used Components
| Component | Uses | Files |
|-----------|------|-------|
| Button | 847 | 124 |
| Text | 623 | 98 |
| Card | 412 | 67 |
| Input | 389 | 45 |
## Unused Components (Deprecation Candidates)
- Accordion (0 uses) - Remove in v3
- Toast (0 uses) - Teams using react-hot-toast instead
- Pagination (2 uses) - Replace with InfiniteScroll
## Variant Analysis
### Button Variants
| Variant | Uses | Percentage |
|---------|------|------------|
| primary | 412 | 49% |
| secondary | 234 | 28% |
| ghost | 156 | 18% |
| destructive | 45 | 5% |
## Recommendations
1. **Remove Accordion** - No adoption, teams prefer custom solutions
2. **Deprecate Toast** - Teams have chosen react-hot-toast
3. **Add IconButton** - 47 instances of icon-only Button pattern foundSuccess Rate
Usage data makes deprecation decisions objective
---
Name
Accessibility-First Components
Description
Build accessibility into components from the start
Detection
accessibility|a11y|aria|keyboard|screen.*reader
When
Creating new components or auditing existing ones
Guidance
Accessibility-First Design
Accessibility is not a feature - it's a requirement.
Component Accessibility Checklist
## Before Shipping Any Component
### Keyboard
- [ ] All interactive elements focusable
- [ ] Focus order logical (tab/shift+tab)
- [ ] Focus visible (2px+ outline)
- [ ] Escape closes overlays
- [ ] Enter/Space activate buttons
- [ ] Arrow keys for composite widgets
### Screen Reader
- [ ] Semantic HTML used (button, not div)
- [ ] Labels present and descriptive
- [ ] States announced (expanded, selected, disabled)
- [ ] Live regions for dynamic content
- [ ] Headings hierarchy logical
### Visual
- [ ] Color contrast 4.5:1+ (text)
- [ ] Color contrast 3:1+ (interactive)
- [ ] Not color-only indicators
- [ ] Text resizable to 200%
- [ ] Reduced motion respectedAccessible Component Example
// Button.tsx - Fully accessible
import { forwardRef } from 'react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost' | 'destructive';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', loading, disabled, children, ...props }, ref) => {
// Use aria-disabled instead of disabled for focus retention
const isDisabled = disabled || loading;
return (
<button
ref={ref}
type="button"
aria-disabled={isDisabled}
aria-busy={loading}
className={cn(
// Base styles
'inline-flex items-center justify-center font-medium',
'transition-colors focus-visible:outline-none',
'focus-visible:ring-2 focus-visible:ring-offset-2',
// Disabled uses opacity, not different colors
isDisabled && 'opacity-50 cursor-not-allowed',
// Size-based touch targets (min 44x44)
size === 'sm' && 'min-h-[36px] px-3 text-sm', // Still touchable
size === 'md' && 'min-h-[44px] px-4 text-base',
size === 'lg' && 'min-h-[52px] px-6 text-lg',
)}
onClick={isDisabled ? undefined : props.onClick}
{...props}
>
{loading && (
<span className="mr-2" aria-hidden="true">
<Spinner size="sm" />
</span>
)}
{children}
{loading && (
<span className="sr-only">Loading, please wait...</span>
)}
</button>
);
}
);
Button.displayName = 'Button';Required ARIA Patterns by Component
| Component | Required ARIA |
|---|---|
| Dialog | role="dialog", aria-modal, aria-labelledby |
| Menu | role="menu", role="menuitem", aria-expanded |
| Tabs | role="tablist", role="tab", role="tabpanel" |
| Combobox | role="combobox", aria-autocomplete, aria-expanded |
| Toast | role="alert" or role="status", aria-live |
| Tooltip | role="tooltip", aria-describedby |
| Dropdown | role="listbox", role="option", aria-selected |
Success Rate
A11y-first components reduce accessibility bugs by 90%
Anti-Patterns
---
Name
Inconsistent Token Naming
Description
Using different naming patterns across the token system
Detection
token|variable|--.color|--.spacing
Why Harmful
Inconsistent naming makes tokens impossible to discover and use correctly. Teams create duplicate tokens, workarounds proliferate, and the system fragments.
What To Do
Establish and enforce a naming convention from day one:
- category-property-variant-state
- color-text-primary, color-background-subtle
- Never: blue-500, textColor, bgGray, colorPrimary
---
Name
No Component Documentation
Description
Shipping components without usage examples and API documentation
Detection
component|export.function|export.const
Why Harmful
Undocumented components don't get used. Teams will build their own or misuse what exists. Documentation is not optional - it's 50% of whether a design system succeeds.
What To Do
Every component needs:
- Clear description of purpose
- Props table with types and defaults
- Usage examples covering common cases
- Accessibility notes
- Do's and Don'ts
---
Name
Tightly Coupled Components
Description
Components that only work together or have hidden dependencies
Detection
import.from.component|require.*component
Why Harmful
Tightly coupled components can't be used independently, create bundle size issues, and make the system rigid. Teams end up importing the whole system for one button.
What To Do
- Each component should be independently usable
- Use composition over coupling
- Lazy load compound components
- Tree-shake friendly exports
---
Name
Breaking Changes Without Migration Path
Description
Removing or changing APIs without deprecation warnings or codemods
Detection
remove|delete|breaking|major
Why Harmful
Breaking changes without migration paths destroy trust. Teams stop updating, fork the system, or abandon it entirely. One bad upgrade experience can kill adoption.
What To Do
- Deprecate with warnings first (3+ months)
- Provide codemods for automated migration
- Document every breaking change
- Support old API during transition period
---
Name
Hardcoded Values Instead of Tokens
Description
Using raw values like
Detection
#[0-9a-fA-F]{6}|\d+px|rgb|rgba
Why Harmful
Hardcoded values bypass the system. They don't respond to theme changes, create inconsistency, and make maintenance impossible. One hardcoded color means theme switching is broken.
What To Do
Use tokens everywhere:
- color: var(--color-primary) not #3B82F6
- padding: var(--spacing-md) not 16px
- Lint for hardcoded values
- Fail CI on token violations
---
Name
Over-Engineered Component APIs
Description
Components with 30+ props trying to handle every use case
Detection
props|interface|type.*Props
Why Harmful
Complex APIs have steep learning curves, confusing documentation, and are impossible to maintain. Teams use 10% of props, the rest is bloat. Every prop is a maintenance burden.
What To Do
- Max 5-7 props before composition
- Use compound components for complex UIs
- Provide sensible defaults
- Separate variants instead of boolean props
---
Name
Ignoring Browser/Device Diversity
Description
Only testing on Chrome desktop with fast internet
Detection
responsive|mobile|safari|firefox|edge
Why Harmful
Real users have old phones, Safari quirks, slow connections, and different screen sizes. A system that only works on Chrome desktop isn't a system - it's a demo.
What To Do
Test matrix:
- Chrome, Safari, Firefox, Edge
- Mobile Safari, Chrome Android
- Slow 3G simulation
- Screen readers (VoiceOver, NVDA)
- Keyboard-only navigation
---
Name
Token Sprawl Without Governance
Description
Adding tokens without process, review, or cleanup
Detection
token|variable|add|new
Why Harmful
Ungoverned tokens multiply. 50 becomes 500, naming diverges, duplicates appear, and nobody knows which to use. Token sprawl makes the system unusable.
What To Do
- Require approval for new tokens
- Regular token audits (quarterly)
- Document why each token exists
- Remove unused tokens aggressively
Design Systems - Sharp Edges
Token Naming Conflicts Break Theme Switching
Id
token-naming-conflicts
Severity
CRITICAL
Description
Inconsistent or conflicting token names make theming impossible
Symptoms
- Theme switch doesn't change all colors
- Some components ignore theme
- "color-blue" exists alongside "blue-primary"
- Multiple tokens for same semantic purpose
Detection Pattern
token|--color|--spacing|variable
Solution
Token Naming Conflict Resolution:
The Problem:
- color-blue-500 (primitive pretending to be semantic)
- textPrimary (camelCase mixing)
- --primary-color (different order)
- blue (too generic)
All four might mean the same thing. Chaos.
The Fix - Strict Naming Convention:
// ENFORCE THIS PATTERN:
// [category]-[property]-[variant]-[state]
const VALID_PATTERNS = {
color: /^color-(text|background|border|icon)-(primary|secondary|muted|inverse|error|success|warning)(-hover|-active|-disabled)?$/,
spacing: /^spacing-(xs|sm|md|lg|xl|2xl)$/,
font: /^font-(family|size|weight|line-height)-.+$/,
};
function validateTokenName(name: string): boolean {
const category = name.split('-')[0];
const pattern = VALID_PATTERNS[category];
return pattern ? pattern.test(name) : false;
}
// Validation script
function auditTokens(tokens: Record<string, any>): string[] {
const issues: string[] = [];
Object.keys(tokens).forEach(name => {
if (!validateTokenName(name)) {
issues.push(`Invalid token name: ${name}`);
}
});
// Check for duplicates by value
const byValue = new Map<string, string[]>();
Object.entries(tokens).forEach(([name, value]) => {
const existing = byValue.get(String(value)) || [];
existing.push(name);
byValue.set(String(value), existing);
});
byValue.forEach((names, value) => {
if (names.length > 1) {
issues.push(`Duplicate value ${value}: ${names.join(', ')}`);
}
});
return issues;
}Prevention: 1. Lint token names in CI 2. Single source of truth (Figma OR code, not both) 3. Require PR approval for new tokens 4. Document the naming convention prominently
References
- https://bradfrost.com/blog/post/naming-tokens-in-design-systems/
Breaking Changes Without Deprecation Destroy Trust
Id
breaking-changes-without-warning
Severity
CRITICAL
Description
Removing or changing APIs without notice breaks consumer codebases
Symptoms
- Teams stop upgrading the design system
- Teams fork their own version
- Angry Slack messages after updates
- We'll just not use the design system
Detection Pattern
upgrade|update|breaking|remove|deprecate
Solution
Breaking Change Protocol:
NEVER DO THIS:
// v2.0.0 - Button
<Button type="primary">Click</Button>
// v3.0.0 - Surprise! API changed
<Button variant="primary">Click</Button> // Everything breaksALWAYS DO THIS:
// v2.5.0 - Add deprecation warning
interface ButtonProps {
/**
* @deprecated Use `variant` instead. Will be removed in v4.0.0
*/
type?: 'primary' | 'secondary';
variant?: 'primary' | 'secondary';
}
function Button({ type, variant, ...props }: ButtonProps) {
if (type && process.env.NODE_ENV !== 'production') {
console.warn(
'[DesignSystem] Button: `type` prop is deprecated. ' +
'Use `variant` instead. Migration: https://design.acme.com/migrate'
);
}
const resolvedVariant = variant ?? type ?? 'primary';
// ...
}
// v3.0.0 - Warning becomes error in dev
if (type && process.env.NODE_ENV === 'development') {
console.error('[DesignSystem] Button: `type` prop removed. Use `variant`.');
}
// v4.0.0 - Remove old prop (announced 6+ months ago)
interface ButtonProps {
variant: 'primary' | 'secondary'; // No more `type`
}Deprecation Timeline: 1. v2.5: Add warning, support both 2. v3.0: Change warning to error in dev 3. v3.5: Provide codemod 4. v4.0: Remove (6+ months after deprecation)
Always provide:
- Migration guide with before/after
- Codemod for automated updates
- Clear timeline
- Slack/email announcement
References
- https://semver.org/
High Adoption Friction Kills Design Systems
Id
adoption-friction
Severity
HIGH
Description
If it's hard to use, teams won't use it
Symptoms
- Low adoption metrics
- Teams building custom components
- The design system is too rigid
- Long onboarding time for new developers
Detection Pattern
install|setup|onboard|start|quick
Solution
Reduce Adoption Friction:
Friction Points and Fixes:
1. INSTALLATION FRICTION Bad: 15 peer dependencies, complex setup
npm install @acme/design-system @acme/tokens @acme/icons
npm install @emotion/react @emotion/styled framer-motion
# Plus 10 more...Good: Single package, zero config
npm install @acme/design-system2. IMPORT FRICTION Bad: Deep imports, multiple sources
import { Button } from '@acme/design-system/components/Button';
import { useTheme } from '@acme/design-system/hooks';
import { colors } from '@acme/tokens';Good: Single entry point
import { Button, useTheme, colors } from '@acme/design-system';3. CONFIGURATION FRICTION Bad: Required configuration before use
// Must set up theme provider, configure tokens, initialize context...
<ThemeProvider theme={customTheme} tokens={tokenConfig} mode="light">
<TokensProvider value={tokens}>
<App />
</TokensProvider>
</ThemeProvider>Good: Works with zero config, customize if needed
// Works immediately
<DesignSystemProvider>
<App />
</DesignSystemProvider>
// Or customize
<DesignSystemProvider theme="dark" brand="acme">
<App />
</DesignSystemProvider>4. DOCUMENTATION FRICTION Bad: Incomplete docs, no examples Good: Every component has:
- Live playground
- Copy-paste examples
- Props table
- Common patterns
Adoption Checklist:
- [ ] npm install is one command
- [ ] First component works in < 5 minutes
- [ ] No required configuration
- [ ] Examples cover 80% of use cases
- [ ] TypeScript autocomplete works
References
- https://bradfrost.com/blog/post/design-system-adoption/
Theme Inheritance Creates Unpredictable Styles
Id
theme-inheritance-issues
Severity
HIGH
Description
Nested themes or improper CSS specificity cause styling chaos
Symptoms
- Components look different in different parts of the app
- Theme overrides don't work consistently
- "Important" declarations everywhere
- Nested dark/light themes behave strangely
Detection Pattern
theme|inherit|nested|!important|specificity
Solution
Theme Inheritance Problems:
The Issue:
// Nested themes = chaos
<ThemeProvider theme="light">
<Card> {/* Light theme */}
<ThemeProvider theme="dark">
<Modal> {/* Dark theme */}
<ThemeProvider theme="light">
<Tooltip> {/* Light again? Or inherited? */}
</Tooltip>
</ThemeProvider>
</Modal>
</ThemeProvider>
</Card>
</ThemeProvider>CSS Custom Properties Solution:
/* Tokens scope to nearest theme ancestor */
[data-theme="light"] {
--color-background: white;
--color-text: black;
}
[data-theme="dark"] {
--color-background: #1a1a1a;
--color-text: white;
}
/* Components use tokens, not raw values */
.card {
background: var(--color-background);
color: var(--color-text);
}Nested Theme Support:
function ThemeProvider({ theme, children }: { theme: 'light' | 'dark'; children: React.ReactNode }) {
return (
<div data-theme={theme} style={{ colorScheme: theme }}>
{children}
</div>
);
}
// Usage - each section respects its theme
<ThemeProvider theme="light">
<MainContent />
<ThemeProvider theme="dark">
<Sidebar /> {/* Dark sidebar in light app */}
</ThemeProvider>
</ThemeProvider>Specificity Rules:
/* BAD: Specificity wars */
.button { background: blue; }
.dark .button { background: darkblue !important; }
.modal .dark .button { background: navy !important !important; } /* Doesn't work */
/* GOOD: Token-based, no specificity issues */
.button {
background: var(--color-button-background);
}
/* Theme changes the variable, not the rule */Testing Nested Themes:
// Test every component in every theme combination
const themes = ['light', 'dark'];
const nesting = [1, 2, 3]; // Nesting levels
themes.forEach(outer => {
themes.forEach(inner => {
test(`Component in ${outer} > ${inner}`, () => {
render(
<ThemeProvider theme={outer}>
<ThemeProvider theme={inner}>
<Button>Test</Button>
</ThemeProvider>
</ThemeProvider>
);
// Verify correct colors
});
});
});References
- https://css-tricks.com/theming-with-variables-globals-and-locals/
Component API Bloat Makes Components Unusable
Id
component-api-bloat
Severity
HIGH
Description
Too many props, variants, and options overwhelm users
Symptoms
- Components with 20+ props
- Prop combinations that conflict
- "How do I make it do X?" questions constantly
- Documentation longer than the component
Detection Pattern
props|interface|variant|option|config
Solution
API Bloat Prevention:
Signs of Bloat:
// 30+ props = unusable
<Button
variant="primary"
size="md"
color="blue"
hoverColor="darkblue"
activeColor="navy"
disabledColor="gray"
textColor="white"
borderRadius="md"
borderWidth={1}
borderColor="transparent"
shadow="sm"
hoverShadow="md"
padding="md"
paddingX="lg"
paddingY="sm"
fontSize="md"
fontWeight="semibold"
lineHeight="tight"
leftIcon={<Plus />}
rightIcon={<Arrow />}
iconSpacing="sm"
loading={false}
loadingText="Loading..."
loadingPosition="left"
disabled={false}
fullWidth={false}
// ... 15 more props
/>Simplification Strategies:
1. VARIANTS OVER PROPS
// Bad: Many boolean props
<Button primary large rounded shadow />
// Good: Variant enum
<Button variant="primary" size="lg" />2. COMPOSITION OVER CONFIGURATION
// Bad: Every layout option as prop
<Card
headerTitle="Settings"
headerAction={<Button>Edit</Button>}
footerLeft={<Text>Updated today</Text>}
footerRight={<Button>Save</Button>}
/>
// Good: Compose with children
<Card>
<Card.Header>
<Card.Title>Settings</Card.Title>
<Button>Edit</Button>
</Card.Header>
<Card.Footer>
<Text>Updated today</Text>
<Button>Save</Button>
</Card.Footer>
</Card>3. SENSIBLE DEFAULTS
// Default everything reasonable
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost'; // default: 'primary'
size?: 'sm' | 'md' | 'lg'; // default: 'md'
// Only 2 props needed for 90% of uses
}
// Most buttons: <Button>Click</Button>
// No props needed!4. PROGRESSIVE DISCLOSURE
// Simple API for simple uses
<Select options={options} onChange={handleChange} />
// Advanced API available when needed
<Select
options={options}
onChange={handleChange}
isMulti
isSearchable
customComponents={{ Option: CustomOption }}
/>Prop Limit Rule:
- 0-5 props: Simple component, good
- 6-10 props: Getting complex, review needed
- 11-15 props: Too complex, split into variants
- 16+ props: Redesign required
References
- https://react-spectrum.adobe.com/react-aria/
Figma-Code Drift Creates Inconsistency
Id
figma-code-drift
Severity
MEDIUM
Description
Design files and code diverge over time
Symptoms
- Designers and developers argue about "correct" values
- Screenshots from Figma don't match production
- Token values differ between design and code
- "The spacing looks off" conversations
Detection Pattern
figma|design.file|token.sync|style.*dictionary
Solution
Preventing Figma-Code Drift:
Root Causes: 1. Manual token entry in both places 2. No sync process 3. Designer changes without notifying devs 4. Dev "fixes" without updating Figma
Solution: Single Source of Truth
Option A: Figma as Source
# .github/workflows/sync-from-figma.yml
name: Sync Tokens from Figma
on:
schedule:
- cron: '0 */4 * * *' # Every 4 hours
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Export from Figma
run: npx figma-export tokens
env:
FIGMA_TOKEN: ${{ secrets.FIGMA_TOKEN }}
- name: Build tokens
run: npx style-dictionary build
- name: Create PR if changed
uses: peter-evans/create-pull-request@v5
with:
title: 'sync: design tokens from Figma'
branch: figma-syncOption B: Code as Source
# Figma plugin reads from deployed tokens
# Designers always see current production valuesOption C: External Source (Recommended)
# tokens.studio.json - Single source
# Syncs to BOTH Figma and code
{
"color": {
"primary": {
"value": "#2563EB",
"type": "color"
}
}
}Drift Detection:
// drift-check.ts - Run in CI
import figmaTokens from './figma-export.json';
import codeTokens from './dist/tokens.json';
function detectDrift(): string[] {
const drift: string[] = [];
Object.keys(figmaTokens).forEach(key => {
if (figmaTokens[key] !== codeTokens[key]) {
drift.push(`${key}: Figma=${figmaTokens[key]}, Code=${codeTokens[key]}`);
}
});
return drift;
}
const drift = detectDrift();
if (drift.length > 0) {
console.error('Token drift detected:');
drift.forEach(d => console.error(` ${d}`));
process.exit(1);
}References
- https://tokens.studio/
Missing Component States Break Interactions
Id
missing-component-states
Severity
MEDIUM
Description
Components missing hover, focus, disabled, or error states
Symptoms
- The button doesn't look clickable
- No visual feedback on hover
- Focus not visible for keyboard users
- Error states undefined or inconsistent
Detection Pattern
hover|focus|disabled|error|state|active
Solution
Complete State Coverage:
Required States for Interactive Components:
// State checklist for Button
const BUTTON_STATES = [
'default', // Resting state
'hover', // Mouse over
'focus', // Keyboard focus (visible ring)
'active', // Being clicked
'disabled', // Not interactive
'loading', // Async operation in progress
];
// State checklist for Input
const INPUT_STATES = [
'default', // Empty, no interaction
'hover', // Mouse over
'focus', // Active editing
'filled', // Has value
'disabled', // Not editable
'readonly', // Viewable, not editable
'error', // Validation failed
'success', // Validation passed (optional)
];
// State checklist for Checkbox
const CHECKBOX_STATES = [
'unchecked',
'checked',
'indeterminate', // Partial selection
'hover',
'focus',
'disabled',
'error',
];CSS Implementation:
.button {
/* Default */
background: var(--color-button-bg);
color: var(--color-button-text);
/* Hover - visible change */
&:hover:not(:disabled) {
background: var(--color-button-bg-hover);
}
/* Focus - ALWAYS visible, 2px+ ring */
&:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
/* Active - feedback on click */
&:active:not(:disabled) {
transform: scale(0.98);
}
/* Disabled - reduced opacity, no pointer */
&:disabled,
&[aria-disabled="true"] {
opacity: 0.5;
cursor: not-allowed;
}
}State Testing:
// Storybook story for all states
export const AllStates: Story = {
render: () => (
<div className="grid grid-cols-3 gap-4">
<Button>Default</Button>
<Button className="pseudo-hover">Hover</Button>
<Button className="pseudo-focus-visible">Focus</Button>
<Button className="pseudo-active">Active</Button>
<Button disabled>Disabled</Button>
<Button loading>Loading</Button>
</div>
),
};
// Visual regression test
test('button states', async ({ page }) => {
await page.goto('/storybook/button--all-states');
await expect(page).toHaveScreenshot('button-states.png');
});References
- https://www.w3.org/WAI/ARIA/apg/patterns/
Version Chaos Creates Dependency Hell
Id
versioning-chaos
Severity
MEDIUM
Description
Multiple versions in same app, incompatible updates
Symptoms
- Which version should I use?
- Different features available in different versions
- Bundled twice due to version mismatch
- Style conflicts between versions
Detection Pattern
version|upgrade|dependency|peer
Solution
Version Management Strategy:
The Problem:
// App's package.json
{
"dependencies": {
"@acme/design-system": "^2.0.0"
}
}
// Shared library also uses design system
{
"dependencies": {
"@acme/design-system": "^1.5.0" // Different version!
}
}
// Result: Two versions bundled, styles conflictSolutions:
1. PEER DEPENDENCIES
// Shared library
{
"peerDependencies": {
"@acme/design-system": ">=1.5.0 <3.0.0"
}
}2. CSS SCOPING
/* Version namespace prevents conflicts */
.ds-v2-button { ... }
.ds-v3-button { ... }
/* Or use CSS layers */
@layer ds-v2, ds-v3;
@layer ds-v2 {
.button { ... }
}3. SINGLETON PATTERN
// Ensure single instance across app
declare global {
interface Window {
__ACME_DS_VERSION__?: string;
}
}
if (window.__ACME_DS_VERSION__ && window.__ACME_DS_VERSION__ !== VERSION) {
console.error(
`Design system version conflict: ${window.__ACME_DS_VERSION__} vs ${VERSION}`
);
}
window.__ACME_DS_VERSION__ = VERSION;4. VERSION COMPATIBILITY MATRIX
| DS Version | React | Emotion | Node |
|------------|-------|---------|------|
| 3.x | >=18 | >=11 | >=18 |
| 2.x | >=17 | >=11 | >=16 |
| 1.x | >=16 | >=10 | >=14 |Release Strategy:
- Major: Breaking changes, 6-month cycle
- Minor: New features, monthly
- Patch: Bug fixes, as needed
- LTS: Support previous major for 12 months
References
- https://semver.org/
Accessibility as Afterthought Creates Exclusion
Id
accessibility-afterthought
Severity
HIGH
Description
Building components without accessibility from the start
Symptoms
- Failed accessibility audits
- Keyboard navigation doesn't work
- Screen readers announce wrong content
- We'll add accessibility later
Detection Pattern
a11y|accessibility|aria|screen.*reader|keyboard
Solution
Accessibility-First Development:
The Problem:
// Inaccessible "button"
<div onClick={handleClick} className="button">
Click me
</div>
// Issues:
// - Not focusable
// - No keyboard activation
// - No role announced
// - No focus indicatorThe Solution:
// Accessible button
<button
type="button"
onClick={handleClick}
className="button"
>
Click me
</button>
// Or if custom element needed:
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
}}
className="button"
>
Click me
</div>Accessibility Checklist per Component:
## Pre-Ship A11y Checklist
### Keyboard
- [ ] Focusable with Tab
- [ ] Activatable with Enter/Space (buttons)
- [ ] Escapable (modals, dropdowns)
- [ ] Arrow key navigation (lists, menus)
- [ ] Focus trap in modals
- [ ] Focus visible (2px+ ring)
### Screen Reader
- [ ] Semantic HTML or ARIA role
- [ ] Accessible name (aria-label or content)
- [ ] State announced (expanded, selected)
- [ ] Live regions for dynamic content
- [ ] Error messages linked to inputs
### Visual
- [ ] 4.5:1 contrast (text)
- [ ] 3:1 contrast (interactive)
- [ ] Not color-only indicators
- [ ] Respects prefers-reduced-motion
- [ ] Works at 200% zoom
### Testing
- [ ] axe-core passes
- [ ] VoiceOver tested
- [ ] NVDA tested
- [ ] Keyboard-only testedAutomated Testing:
// In every component test
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('Button is accessible', async () => {
const { container } = render(<Button>Click</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});References
- https://www.w3.org/WAI/WCAG21/quickref/
Design Systems - Validations
Hardcoded Color Values
Id
hardcoded-colors
Description
Colors should use design tokens, not hardcoded hex/rgb values
Pattern
(#[0-9a-fA-F]{3,8}|rgb\(|rgba\(|hsl\(|hsla\()
File Glob
*/.{css,scss,less,tsx,jsx,ts,js}
Match
present
Exclude Pattern
(var\(--|theme\.|tokens\.|colors\.)
Message
Hardcoded color found. Use design token instead: var(--color-*)
Severity
warning
Autofix
Test Cases
Should Match
- background: #3B82F6;
- color: rgb(59, 130, 246);
- border-color: rgba(0, 0, 0, 0.1);
- backgroundColor: '#2563EB'
- fill: hsl(220, 90%, 56%);
Should Not Match
- background: var(--color-primary);
- color: theme.colors.primary
- backgroundColor: colors.blue[500]
- / #3B82F6 is primary blue /
Hardcoded Spacing Values
Id
hardcoded-spacing
Description
Spacing should use design tokens, not arbitrary pixel values
Pattern
(?<!line-height:\s*)(\d+)px
File Glob
*/.{css,scss,less}
Match
present
Exclude Pattern
(var\(--|spacing\.|space\.)
Message
Hardcoded spacing found. Use spacing token: var(--spacing-*)
Severity
info
Autofix
Test Cases
Should Match
- padding: 16px;
- margin: 24px 12px;
- gap: 8px;
- width: 200px;
Should Not Match
- padding: var(--spacing-md);
- margin: var(--spacing-lg);
- line-height: 24px;
- / 16px is our base unit /
Missing Component Documentation
Id
missing-component-docs
Description
Components should have JSDoc documentation
Pattern
export\s+(const|function)\s+[A-Z][a-zA-Z]+\s*[=\(]
File Glob
/components//*.{tsx,jsx}
Match
present
Context Pattern
/\\[\s\S]?\/
Message
Component missing JSDoc documentation. Add description, props, and example.
Severity
warning
Autofix
Test Cases
Should Match
- export const Button = (
- export function Card({
- export const TextField = forwardRef(
Should Not Match
- const helper = (
- function internalUtil(
Missing Display Name for forwardRef
Id
missing-display-name
Description
forwardRef components need displayName for debugging
Pattern
forwardRef\s*<
File Glob
*/.{tsx,jsx}
Match
present
Context Pattern
\.displayName\s*=
Context Lines After
Message
forwardRef component missing displayName. Add: Component.displayName = 'Component'
Severity
warning
Autofix
Test Cases
Should Match
- forwardRef<HTMLButtonElement, ButtonProps>
- forwardRef<HTMLInputElement>((props, ref)
Clickable Div Anti-pattern
Id
div-onclick-antipattern
Description
Clickable elements should be buttons, not divs
Pattern
<div[^>]*onClick
File Glob
*/.{tsx,jsx}
Match
present
Message
Clickable div found. Use <button> or add role='button', tabIndex, keyboard handlers.
Severity
error
Autofix
Test Cases
Should Match
- <div onClick={handleClick}>
- <div className='btn' onClick={click}>
Should Not Match
- <button onClick={handleClick}>
- <div role='button' tabIndex={0} onClick={handleClick}>
Missing Focus Visible Styles
Id
missing-focus-styles
Description
Interactive elements need visible focus styles for keyboard users
Pattern
focus:
File Glob
*/.{tsx,jsx,css,scss}
Match
absent
Context Pattern
(button|input|select|a|\[role)
Message
Focus styles may be missing. Add focus-visible ring for keyboard accessibility.
Severity
warning
Autofix
Test Cases
Should Not Match
- focus:ring-2 focus:ring-offset-2
- focus-visible:outline
- &:focus-visible { outline: 2px solid
CSS Important Override
Id
important-override
Description
Avoid !important - indicates specificity problems
Pattern
!important
File Glob
*/.{css,scss,less}
Match
present
Message
!important found. Refactor to avoid specificity issues.
Severity
warning
Autofix
Test Cases
Should Match
- color: red !important;
- display: none !important
Should Not Match
- / sometimes !important is needed /
Inconsistent Token Naming
Id
inconsistent-token-naming
Description
Token names should follow category-property-variant pattern
Pattern
--[a-zA-Z]+[A-Z]|--[a-z]+_[a-z]+|--[A-Z]
File Glob
*/.{css,scss,ts,js}
Match
present
Message
Inconsistent token naming. Use kebab-case: --category-property-variant
Severity
warning
Autofix
Test Cases
Should Match
- --colorPrimary
- --color_primary
- --ColorPrimary
- --textColor
Should Not Match
- --color-primary
- --color-text-muted
- --spacing-md
Missing Accessible Name
Id
missing-aria-label
Description
Icon-only buttons need aria-label
Pattern
<(button|Button)[^>]>[^<]<(Icon|svg|img)[^>]/?>[^<]</(button|Button)>
File Glob
*/.{tsx,jsx}
Match
present
Context Pattern
aria-label
Message
Icon-only button missing aria-label. Add aria-label for screen readers.
Severity
error
Autofix
Test Cases
Should Match
- <button><Icon /></button>
- <Button><CloseIcon /></Button>
Should Not Match
- <button aria-label='Close'><Icon /></button>
- <button><Icon /> Close</button>
Missing Loading State in Async Buttons
Id
missing-loading-state
Description
Buttons triggering async actions should have loading state
Pattern
(onClick|onSubmit)[^}]*await
File Glob
*/.{tsx,jsx}
Match
present
Context Pattern
(loading|isLoading|pending)
Message
Async action without loading state. Add loading prop to prevent double-clicks.
Severity
info
Autofix
Test Cases
Should Match
- onClick={async () => { await submit(); }}
- onSubmit={async (e) => { await handleSubmit(); }}
Missing Error Boundary
Id
missing-error-boundary
Description
Component sections should have error boundaries
Pattern
export\s+(default\s+)?function\s+[A-Z][a-zA-Z]*Page
File Glob
/pages//*.{tsx,jsx}
Match
present
Context Pattern
ErrorBoundary
Message
Page component may need error boundary for resilience.
Severity
info
Autofix
Primitive Token Used Directly
Id
token-primitive-in-component
Description
Components should use semantic tokens, not primitives
Pattern
(blue|red|green|gray|slate|zinc)-(50|100|200|300|400|500|600|700|800|900)
File Glob
*/.{tsx,jsx,css,scss}
Match
present
Message
Primitive color token used directly. Use semantic token (color-primary, color-text-muted).
Severity
warning
Autofix
Test Cases
Should Match
- text-blue-500
- bg-gray-100
- border-red-600
Should Not Match
- text-primary
- bg-background
- border-error
Magic Z-Index Values
Id
magic-number-z-index
Description
Z-index should use defined scale, not magic numbers
Pattern
z-index:\s*\d{2,}
File Glob
*/.{css,scss}
Match
present
Message
Magic z-index found. Use z-index scale: var(--z-dropdown), var(--z-modal), etc.
Severity
info
Autofix
Test Cases
Should Match
- z-index: 9999;
- z-index: 100;
- z-index: 50;
Should Not Match
- z-index: var(--z-modal);
- z-index: 1;
- z-index: -1;