Write complete component specifications that capture visual values, behavioral intent, accessibility requirements, and implementation notes — everything a developer needs to implement a component correctly without asking the designer.
A Figma file shows what a component looks like. A component spec document explains how it behaves, why design decisions were made, and what the developer needs to know beyond pixels. The gap between these two — showing versus explaining — is where most implementation errors originate.
Good component spec documentation covers five dimensions: visual specification (exact values for all states), behavioral specification (interactions and edge cases), accessibility specification (ARIA, keyboard, focus), implementation notes (specific technical guidance), and the component API contract (how design properties map to code props).
This lesson covers writing complete spec documentation that eliminates clarification conversations and enables accurate, accessible implementations.
Visual specification documents all values that appear in the Figma Inspect panel, organized by state, size, and variant. It supplements Figma (which shows one state at a time) with a complete reference.
Component spec header:
1## Button Component
2**Status**: Final — Sprint 3
3**Version**: 2.1
4**Figma**: [Link to main component]
5**Storybook**: [Link to stories]
6**Implementation**: src/components/ui/Button.tsx
7**Last updated**: 2026-06-01 by @designerVariant matrix:
1### Variants
2| Variant | Use case | When to use |
3|-----------|-----------------------------|------------------------------------|
4| Primary | Main CTA | One per view, most important action |
5| Secondary | Supporting action | Multiple per view allowed |
6| Ghost | Tertiary, low emphasis | In toolbars, dense UIs |
7| Danger | Destructive actions | Delete, remove, revoke |
8| Link | Navigation-style action | In body text, inline |Size specification:
1### Sizes
2| Size | Height | Padding (Y×X) | Font | Min-width | Use case |
3|------|--------|---------------|------|-----------|------------------|
4| SM | 32px | 6px 12px | 13px | 64px | Compact UI, tables |
5| MD | 40px | 9px 16px | 14px | 80px | Default |
6| LG | 48px | 12px 20px | 15px | 96px | Hero CTAs, forms |State specification:
1### States (Primary variant)
2| State | Background | Text | Border | Shadow |
3|----------|------------|---------|-----------|-------------------------------------|
4| Default | #2563EB | #FFFFFF | none | none |
5| Hover | #1D4ED8 | #FFFFFF | none | 0 4px 16px rgb(37 99 235 / 0.25) |
6| Focus | #2563EB | #FFFFFF | none | 0 0 0 3px rgb(37 99 235 / 0.3) |
7| Active | #1E40AF | #FFFFFF | none | none |
8| Disabled | #E5E7EB | #9CA3AF | none | none |
9| Loading | #2563EB | transp. | none | none + spinner overlay |Transition specifications:
1### Transitions
2- Background color: 150ms cubic-bezier(0, 0, 0.2, 1)
3- Box shadow: 150ms cubic-bezier(0, 0, 0.2, 1)
4- Scale (active): 100ms ease
5- Loading spinner: rotate 600ms linear infiniteStatic designs cannot capture behavior. Behavioral specification documents how the component responds to user interaction beyond visual state changes.
Keyboard interaction specification:
1### Keyboard Behavior
2| Key | Action |
3|------------|---------------------------------|
4| Tab | Moves focus to button |
5| Enter | Activates button (same as click)|
6| Space | Activates button (same as click)|
7| Escape | No effect (unless in a dialog) |
8
9### Focus Management
10- Focus ring appears on :focus-visible (keyboard only, not mouse click)
11- Focus ring: 3px solid #2563EB with 2px offset
12- Loading state: focus ring still visible
13- Disabled state: button is NOT focusable (use tabindex=-1 or native disabled attribute)Edge cases:
1### Edge Cases
21. Text overflow: if label exceeds 32 characters, truncate with ellipsis
3 Do not: allow text to wrap to multiple lines
4 CSS: white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
5
62. Icon + text: icon size is always 20px, gap 8px, icon first
7 Do not: allow icon to be after the text (breaks LTR reading)
8
93. Full width: in mobile forms, buttons can be full-width
10 Use: width: 100% or align-self: stretch in flex container
11
124. Inside disabled form: when parent fieldset is disabled,
13 button should also appear disabled (CSS: fieldset:disabled .btn { ... })
14
155. Loading state duration: loading state starts on click and ends when
16 the async operation resolves or rejects (not timer-based)Animation specification:
1### Loading State Behavior
21. User clicks → immediate visual feedback: button stays full opacity
32. Spinner appears after 100ms delay (prevents flash for fast operations)
43. Button label hidden: color: transparent (not display: none — preserves width)
54. Cursor: wait (shows browser loading cursor)
65. On success: spinner fades out, label fades in (150ms)
76. On error: button returns to default state, error message shown elsewhereAccessibility specification documents the ARIA roles, attributes, and behavior required for the component to be usable by keyboard-only and screen reader users.
ARIA specification template:
1### Accessibility
2**Role**: button (native `<button>` element — never use div)
3**Required attributes**:
4- None when visible text label is present
5- aria-label="[action description]" when icon-only
6- aria-busy="true" during loading state
7- aria-disabled="true" when disabled but still in tab order (rare)
8
9**Forbidden patterns**:
10- Never: `<div onclick="...">` (not keyboard accessible)
11- Never: `<a href="#">` for non-navigation actions
12- Never: `disabled` attribute alone without visual disabled styling
13
14**Screen reader behavior**:
15- Default: reads 'Enroll now, button'
16- Loading: reads 'Enroll now, button, loading' (via aria-busy)
17- Disabled: reads 'Enroll now, button, grayed out / dimmed'
18- Icon only: reads '[aria-label value], button'Color contrast requirements:
1### Color Contrast
2| State | Contrast ratio | WCAG requirement | Pass? |
3|----------|---------------|-----------------|-------|
4| Default | 4.8:1 (white on #2563EB) | AA Large text ≥3:1 | ✓ |
5| Hover | 6.1:1 (white on #1D4ED8) | AA Large text ≥3:1 | ✓ |
6| Disabled | 2.1:1 (#9CA3AF on #E5E7EB)| WCAG exemption* | ✓ |
7
8* Disabled controls are exempt from contrast requirements (WCAG 1.4.3)
9 However: we aim for ≥3:1 even on disabled to maintain readabilityFocus management edge cases:
1### Focus Management
2- After click: focus remains on button unless navigating to new page
3- In dialogs: button inside dialog — on dialog close, return focus to
4 the element that triggered the dialog (not the dialog's close button)
5- Loading completion: focus remains on button, read result via aria-live
6 on a status region (not on the button itself)The component API contract bridges design properties (Figma variant properties) and implementation props (React/Vue component API). When this document exists, developers and designers speak the same language.
API contract format:
1## Button Component API
2
3### TypeScript Props Interfaceinterface ButtonProps {
// Maps to Figma 'Variant' property
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'link';
// Maps to Figma 'Size' property
size?: 'sm' | 'md' | 'lg';
// Maps to Figma 'Icon: Yes/No' property
icon?: ReactNode;
// Maps to Figma 'Icon Position' property (if exists)
iconPosition?: 'left' | 'right';
// Maps to Figma 'Loading' boolean property
loading?: boolean;
// Maps to Figma 'State: Disabled' variant
disabled?: boolean;
// Full width — Figma: width constraint 'fill container'
fullWidth?: boolean;
// Accessible label for icon-only buttons
aria-label?: string;
// Standard HTML button props
type?: 'button' | 'submit' | 'reset';
onClick?: (e: MouseEvent) => void;
className?: string;
children: ReactNode;
}
1
2Default values:| Prop | Default | Figma equivalent |
|----------|------------|-------------------------------|
| variant | 'primary' | Variant: Primary |
| size | 'md' | Size: MD |
| loading | false | Loading: Off |
| disabled | false | State: Default |
| type | 'button' | (HTML attribute, not in Figma)|
1
2Usage examples:// Primary CTA (most common)
<Button onClick={handleEnroll}>Enroll now</Button>
// Loading state
<Button loading onClick={handlePayment}>Processing...</Button>
// Icon + label
<Button icon={<PlayIcon />} variant="secondary">
Start course
</Button>
// Icon only (requires aria-label)
<Button icon={<SettingsIcon />} size="sm" aria-label="Course settings" />
// Full width in mobile form
<Button fullWidth>Create account</Button>
// Danger action
<Button variant="danger" onClick={handleDelete}>Delete course</Button>
A full component specification document for the course enrollment button, including all visual states, behavior, accessibility, and the implementation API.
1# Course Enrollment Button — Component Specification
2
3## Overview
4**Component**: EnrollButton
5**Location**: src/components/course/EnrollButton.tsx
6**Figma**: figma.com/file/xxx — Components / Course / Enroll Button
7**Status**: Final v1.2
8
9## Variants
10- Default: 'Enroll — $X' (for paid courses)
11- Free: 'Start learning — Free'
12- Enrolled: 'Continue learning' (shows progress %)
13- Completed: 'Restart course'
14
15## Visual Specification
16| State | Background | Text | Notes |
17|----------|------------|---------|-------------------------|
18| Default | #2563EB | white | Paid course primary CTA |
19| Hover | #1D4ED8 | white | Darker blue |
20| Loading | #2563EB | transp. | Spinner overlay |
21| Enrolled | #16A34A | white | Green success state |
22| Disabled | #E5E7EB | #9CA3AF | Not purchasable |
23
24Typography: Inter 15px/600, full width (w:100%), height 48px
25Border radius: 8px | Transition: 150ms ease all properties
26
27## Behavioral Specification
28
29### Click behavior by state
30Default (paid):
31 - Opens checkout modal (not page navigation)
32 - Button enters loading state immediately on click
33 - On success: transitions to Enrolled state (green)
34 - On cancel: returns to Default state
35 - On error: returns to Default, shows toast error
36
37Free course:
38 - Starts enrollment immediately (no checkout)
39 - Loading 500ms → Enrolled state (green)
40 - On error: toast 'Failed to enroll, please try again'
41
42Enrolled (continue):
43 - Navigates to last lesson in progress
44 - No loading state (navigation, not async)
45 - If no progress: navigates to lesson 1
46
47### Edge Cases
48- If user not logged in: redirect to /login?next=[course-url]
49- If course at capacity: disabled state, tooltip 'Currently full'
50- On mobile: button is always full-width
51- Price display: format currency by locale (en-US: $49, en-IN: ₹3,999)
52
53## Accessibility
54HTML element: <button> (never div or anchor)
55aria-busy='true' during loading state
56aria-label='[variant-specific description]' overrides visible text
57 when visible text is insufficient (e.g., 'Enroll in CSS Mastery for $49')
58
59Keyboard:
60 Enter or Space: activates button
61 Escape: closes checkout modal (not button behavior)
62
63Focus: visible 3px blue ring on keyboard focus
64 Remains on button after enrollment (announce result via aria-live)
65
66## React Props API
67```typescript
68interface EnrollButtonProps {
69 courseId: string;
70 price: number | 'free';
71 currency: string;
72 enrolled: boolean;
73 progress?: number; // 0-100 if enrolled
74 completed?: boolean;
75 disabled?: boolean;
76 className?: string;
77 onSuccess?: (enrollmentId: string) => void;
78 onError?: (error: Error) => void;
79}
80```
81
82## Implementation Notes
831. Price formatting: use Intl.NumberFormat(locale, { style: 'currency', currency })
842. Enrollment API: POST /progress/enroll/:courseId — returns { enrollmentId }
853. Loading state: set immediately on click, clear on API response (success or error)
864. Green enrolled state: animate background-color transition, not instant switch
875. Progress indicator (enrolled): shows 'Continue — 68%' — use Math.round()
88 Do not show decimals (68.5% → 68%)
896. Track enrollment attempts: analytics.track('enroll_attempt', { courseId, price })This specification eliminates every clarifying question a developer would need to ask: what happens on API error? What if the user is not logged in? How is the price formatted? What ARIA attributes are needed? What is the enrollment API endpoint? Each question is answered in the spec before it can be asked. The TypeScript interface makes the API contract machine-readable — IDEs will validate usage against the spec automatically.
The next lesson covers Design Sprints — the agile workflow where design and development timelines are coordinated for efficient sprint delivery without design becoming the bottleneck.
Your progress is saved in this browser, on this device only.