CODITECT Design Pattern Library
Version: 1.0
Philosophy: Atomic Design - Composable, Testable, Reusable
Structure: Atoms → Molecules → Organisms → Templates → Pages
Overview: The Lego System
ATOMS (Basic building blocks)
↓
MOLECULES (Simple combinations)
↓
ORGANISMS (Complex UI sections)
↓
TEMPLATES (Page layouts)
↓
PAGES (Specific instances)
Key Principle: Every pattern has:
- Code snippet (copy-paste ready)
- Test criteria (validation rules)
- Usage examples (when to use)
- Variants (different states/sizes)
- Accessibility (WCAG compliance)
ATOMS
A1: Button
Purpose: Trigger actions, submit forms, navigate
Test Criteria:
visual:
- height: 40px (44px minimum touch target)
- border_radius: 6px
- font_weight: 500
- font_size: 14px
interaction:
- cursor: pointer on hover
- focus_visible: true (blue outline)
- disabled_state: reduced opacity
accessibility:
- keyboard_navigable: true
- aria_label: present if icon-only
- min_contrast_ratio: 4.5:1
Variants:
<!-- Primary Button -->
<button class="btn btn-primary" data-testid="btn-primary">
Create Project
</button>
<!-- Secondary Button -->
<button class="btn btn-secondary" data-testid="btn-secondary">
Cancel
</button>
<!-- Danger Button -->
<button class="btn btn-danger" data-testid="btn-danger">
Delete
</button>
<!-- Icon Button -->
<button class="btn-icon" aria-label="Close" data-testid="btn-icon">
<svg width="16" height="16">
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<!-- Loading State -->
<button class="btn btn-primary" disabled data-testid="btn-loading">
<span class="spinner"></span>
Processing...
</button>
CSS:
.btn {
height: 40px;
padding: 0 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.btn:focus-visible {
outline: 2px solid var(--blue-500);
outline-offset: 2px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: var(--blue-500);
color: var(--white);
}
.btn-primary:hover:not(:disabled) {
background: var(--blue-600);
}
.btn-secondary {
background: var(--gray-200);
color: var(--gray-900);
}
.btn-secondary:hover:not(:disabled) {
background: var(--gray-300);
}
.btn-danger {
background: var(--red-100);
color: var(--red-700);
}
.btn-danger:hover:not(:disabled) {
background: var(--red-200);
}
.btn-icon {
width: 40px;
height: 40px;
padding: 0;
background: transparent;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon:hover {
background: var(--gray-100);
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin-right: 8px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
Test Cases:
describe('Button Atom', () => {
test('primary button has correct styles', () => {
const btn = document.querySelector('[data-testid="btn-primary"]');
expect(getComputedStyle(btn).height).toBe('40px');
expect(getComputedStyle(btn).borderRadius).toBe('6px');
});
test('button is keyboard accessible', () => {
const btn = document.querySelector('[data-testid="btn-primary"]');
btn.focus();
expect(document.activeElement).toBe(btn);
});
test('disabled button is not clickable', () => {
const btn = document.querySelector('[data-testid="btn-loading"]');
const clickHandler = jest.fn();
btn.addEventListener('click', clickHandler);
btn.click();
expect(clickHandler).not.toHaveBeenCalled();
});
});
A2: Input
Purpose: Text entry, search, forms
Test Criteria:
visual:
- height: 40px
- border: 1px solid var(--gray-200)
- border_radius: 6px
- padding: 0 12px
interaction:
- focus_border: var(--blue-500)
- placeholder_visible: true
- error_state: red border
accessibility:
- label_associated: true
- aria_invalid: true on error
- autocomplete: appropriate
Variants:
<!-- Text Input -->
<input
type="text"
class="input"
placeholder="Enter text..."
aria-label="Text input"
data-testid="input-text"
/>
<!-- Search Input -->
<input
type="search"
class="input input-search"
placeholder="Search..."
aria-label="Search"
data-testid="input-search"
/>
<!-- Error State -->
<input
type="email"
class="input input-error"
placeholder="email@example.com"
aria-invalid="true"
aria-describedby="email-error"
data-testid="input-error"
/>
<span id="email-error" class="input-error-text">
Invalid email address
</span>
<!-- With Icon -->
<div class="input-wrapper">
<svg class="input-icon">
<path d="..."/>
</svg>
<input
type="text"
class="input input-with-icon"
placeholder="Search..."
/>
</div>
CSS:
.input {
width: 100%;
height: 40px;
padding: 0 12px;
border: 1px solid var(--gray-200);
border-radius: 6px;
font-size: 14px;
color: var(--gray-900);
outline: none;
transition: border-color 0.2s;
font-family: inherit;
}
.input:focus {
border-color: var(--blue-500);
}
.input::placeholder {
color: var(--gray-400);
}
.input-error {
border-color: var(--red-500);
}
.input-error:focus {
border-color: var(--red-500);
}
.input-error-text {
display: block;
margin-top: 4px;
font-size: 12px;
color: var(--red-500);
}
.input-wrapper {
position: relative;
}
.input-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
color: var(--gray-400);
pointer-events: none;
}
.input-with-icon {
padding-left: 36px;
}
/* Textarea variant */
textarea.input {
height: auto;
padding: 12px;
resize: vertical;
min-height: 80px;
}
/* Select variant */
select.input {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%239CA3AF' stroke-width='2'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 36px;
}
A3: Status Dot
Purpose: Visual status indicator
Test Criteria:
visual:
- size: 8px
- border_radius: 50%
- colors: green/yellow/red/gray
semantic:
- green: success/active/healthy
- yellow: warning/in-progress/pending
- red: error/blocked/critical
- gray: inactive/disabled/unknown
accessibility:
- aria_label: status description
- not_color_only: accompanied by text
Variants:
<!-- Status Dots -->
<div class="status-dot status-success"
role="img"
aria-label="Active"
data-testid="dot-success">
</div>
<div class="status-dot status-warning"
role="img"
aria-label="Pending"
data-testid="dot-warning">
</div>
<div class="status-dot status-error"
role="img"
aria-label="Blocked"
data-testid="dot-error">
</div>
<div class="status-dot status-inactive"
role="img"
aria-label="Inactive"
data-testid="dot-inactive">
</div>
<!-- Pulsing Variant (Live Activity) -->
<div class="status-dot status-success status-pulse"
role="img"
aria-label="Live">
</div>
CSS:
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.status-success {
background: var(--green-500);
}
.status-warning {
background: var(--yellow-500);
}
.status-error {
background: var(--red-500);
}
.status-inactive {
background: var(--gray-400);
}
.status-pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Size variants */
.status-dot-sm {
width: 6px;
height: 6px;
}
.status-dot-lg {
width: 12px;
height: 12px;
}
A4: Avatar
Purpose: User identity representation
Test Criteria:
visual:
- size: 32px (default)
- border_radius: 50%
- border: 2px solid white (when stacked)
content:
- image: user photo (preferred)
- initials: fallback (1-2 letters)
- icon: generic fallback
accessibility:
- alt_text: user name
- aria_label: user identifier
Variants:
<!-- Image Avatar -->
<img
src="/user-photo.jpg"
alt="Sarah Chen"
class="avatar"
data-testid="avatar-image"
/>
<!-- Initial Avatar -->
<div class="avatar avatar-initials"
style="background: var(--blue-500);"
aria-label="John Doe"
data-testid="avatar-initials">
JD
</div>
<!-- Icon Avatar (Fallback) -->
<div class="avatar avatar-icon"
aria-label="Unknown user"
data-testid="avatar-icon">
👤
</div>
<!-- Size Variants -->
<div class="avatar avatar-sm">👤</div>
<div class="avatar">👤</div>
<div class="avatar avatar-lg">👤</div>
CSS:
.avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.avatar-initials {
display: flex;
align-items: center;
justify-content: center;
color: var(--white);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.avatar-icon {
display: flex;
align-items: center;
justify-content: center;
background: var(--gray-200);
font-size: 16px;
}
/* Size variants */
.avatar-sm {
width: 24px;
height: 24px;
font-size: 10px;
}
.avatar-lg {
width: 48px;
height: 48px;
font-size: 18px;
}
/* Stacked avatars (use with negative margin) */
.avatar-stacked {
border: 2px solid var(--white);
}
A5: Badge
Purpose: Count, status label, category tag
Test Criteria:
visual:
- padding: 4px 8px
- border_radius: 10px (pill shape)
- font_size: 12px
- font_weight: 500
semantic:
- count: numeric values
- status: text labels
- category: classification tags
accessibility:
- aria_label: full description
- not_interactive: static display only
Variants:
<!-- Count Badge -->
<span class="badge badge-count"
aria-label="3 unread items"
data-testid="badge-count">
3
</span>
<!-- Status Badges -->
<span class="badge badge-success" data-testid="badge-success">
Active
</span>
<span class="badge badge-warning" data-testid="badge-warning">
Pending
</span>
<span class="badge badge-error" data-testid="badge-error">
Blocked
</span>
<!-- Category Tag -->
<span class="badge badge-neutral" data-testid="badge-tag">
Design
</span>
<!-- Small Variant -->
<span class="badge badge-sm badge-count">5</span>
CSS:
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
line-height: 1;
white-space: nowrap;
}
.badge-count {
min-width: 20px;
text-align: center;
background: var(--red-500);
color: var(--white);
}
.badge-success {
background: var(--green-100);
color: var(--green-700);
}
.badge-warning {
background: var(--yellow-100);
color: var(--yellow-700);
}
.badge-error {
background: var(--red-100);
color: var(--red-700);
}
.badge-neutral {
background: var(--gray-200);
color: var(--gray-700);
}
.badge-sm {
padding: 2px 6px;
font-size: 10px;
min-width: 16px;
}
A6: Progress Bar
Purpose: Visual progress/usage indicator
Test Criteria:
visual:
- height: 6px (default)
- border_radius: 3px
- background: gray-100
- fill: semantic color
behavior:
- width: 0-100%
- transition: smooth (0.3s)
- aria_valuenow: current value
accessibility:
- role: progressbar
- aria_valuemin: 0
- aria_valuemax: 100
- aria_valuenow: current percentage
Variants:
<!-- Basic Progress -->
<div class="progress-bar"
role="progressbar"
aria-valuenow="65"
aria-valuemin="0"
aria-valuemax="100"
data-testid="progress-basic">
<div class="progress-fill" style="width: 65%;"></div>
</div>
<!-- With Label -->
<div class="progress-container">
<div class="progress-bar" role="progressbar" aria-valuenow="85">
<div class="progress-fill" style="width: 85%;"></div>
</div>
<span class="progress-label">85%</span>
</div>
<!-- Semantic Colors -->
<div class="progress-bar">
<div class="progress-fill progress-success" style="width: 80%;"></div>
</div>
<div class="progress-bar">
<div class="progress-fill progress-warning" style="width: 50%;"></div>
</div>
<div class="progress-bar">
<div class="progress-fill progress-error" style="width: 95%;"></div>
</div>
<!-- Indeterminate (Loading) -->
<div class="progress-bar progress-indeterminate">
<div class="progress-fill"></div>
</div>
CSS:
.progress-bar {
width: 100%;
height: 6px;
background: var(--gray-100);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--blue-500);
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-success {
background: var(--green-500);
}
.progress-warning {
background: var(--yellow-500);
}
.progress-error {
background: var(--red-500);
}
.progress-container {
display: flex;
align-items: center;
gap: 12px;
}
.progress-label {
font-size: 14px;
font-weight: 500;
color: var(--gray-900);
min-width: 40px;
text-align: right;
}
/* Indeterminate animation */
.progress-indeterminate .progress-fill {
width: 30%;
animation: indeterminate 1.5s ease-in-out infinite;
}
@keyframes indeterminate {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
/* Size variants */
.progress-bar-sm {
height: 4px;
}
.progress-bar-lg {
height: 8px;
}
A7: Skeleton (Loading Placeholder)
Purpose: Content placeholder during loading
Test Criteria:
visual:
- background: gray gradient
- border_radius: 4px
- animation: shimmer effect
behavior:
- replaces_actual_content: true
- maintains_layout: true
- aria_busy: true on container
accessibility:
- aria_label: "Loading content"
- sr_only_text: "Please wait"
Variants:
<!-- Text Skeleton -->
<div class="skeleton skeleton-text" data-testid="skeleton-text"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
<div class="skeleton skeleton-text" style="width: 60%;"></div>
<!-- Title Skeleton -->
<div class="skeleton skeleton-title" data-testid="skeleton-title"></div>
<!-- Circle Skeleton (Avatar) -->
<div class="skeleton skeleton-circle" data-testid="skeleton-circle"></div>
<!-- Rectangle Skeleton (Image) -->
<div class="skeleton skeleton-rect" data-testid="skeleton-rect"></div>
<!-- Complete Card Skeleton -->
<div class="skeleton-card" aria-busy="true" aria-label="Loading content">
<div class="skeleton skeleton-circle"></div>
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
</div>
CSS:
.skeleton {
background: linear-gradient(
90deg,
var(--gray-100) 25%,
var(--gray-200) 50%,
var(--gray-100) 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.skeleton-text {
width: 100%;
height: 14px;
margin-bottom: 8px;
}
.skeleton-title {
width: 60%;
height: 20px;
margin-bottom: 12px;
}
.skeleton-circle {
width: 32px;
height: 32px;
border-radius: 50%;
}
.skeleton-rect {
width: 100%;
height: 200px;
}
/* Card skeleton layout */
.skeleton-card {
padding: 20px;
background: var(--white);
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.skeleton-card > .skeleton {
margin-bottom: 12px;
}
.skeleton-card > .skeleton:last-child {
margin-bottom: 0;
}
MOLECULES
M1: Status Indicator
Purpose: Combined dot + text for status display
Test Criteria:
composition:
- atom: status_dot
- atom: text_label
behavior:
- dot_and_text_aligned: true
- color_consistent: true
- spacing: 8px gap
accessibility:
- screenreader_reads_both: true
- semantic_meaning: clear
Code:
<div class="status-indicator" data-testid="status-indicator">
<div class="status-dot status-success"></div>
<span class="status-label">Active</span>
</div>
CSS:
.status-indicator {
display: inline-flex;
align-items: center;
gap: 8px;
}
.status-label {
font-size: 14px;
color: var(--gray-700);
}
M2: Avatar Stack
Purpose: Display multiple team members with overlap
Test Criteria:
composition:
- atoms: multiple_avatars
- layout: overlapping
behavior:
- overlap: -8px margin
- z_index: stacking order
- max_visible: 5 (show +N for more)
accessibility:
- aria_label: "Team members: Name1, Name2..."
- group_role: "group"
Code:
<div class="avatar-stack"
role="group"
aria-label="Team members: Sarah, Mike, Emma"
data-testid="avatar-stack">
<img src="/sarah.jpg" alt="Sarah" class="avatar avatar-stacked" />
<img src="/mike.jpg" alt="Mike" class="avatar avatar-stacked" />
<img src="/emma.jpg" alt="Emma" class="avatar avatar-stacked" />
<div class="avatar avatar-initials avatar-stacked" style="background: var(--gray-500);">
+2
</div>
</div>
CSS:
.avatar-stack {
display: flex;
align-items: center;
}
.avatar-stack > .avatar {
margin-left: -8px;
border: 2px solid var(--white);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.avatar-stack > .avatar:first-child {
margin-left: 0;
}
.avatar-stack > .avatar:hover {
z-index: 10;
}
M3: Search Input
Purpose: Input with search icon and clear button
Test Criteria:
composition:
- atom: input
- atom: icon (search)
- atom: button (clear)
behavior:
- clear_button_appears: when value exists
- enter_key: submits search
- escape_key: clears input
accessibility:
- aria_label: "Search"
- clear_button_label: "Clear search"
Code:
<div class="search-input" data-testid="search-input">
<svg class="search-icon" width="16" height="16">
<circle cx="7" cy="7" r="6" stroke="currentColor" fill="none"/>
<path d="M11 11l4 4" stroke="currentColor"/>
</svg>
<input
type="search"
class="input search-field"
placeholder="Search..."
aria-label="Search"
/>
<button class="search-clear btn-icon" aria-label="Clear search">
×
</button>
</div>
CSS:
.search-input {
position: relative;
width: 100%;
max-width: 400px;
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--gray-400);
pointer-events: none;
z-index: 1;
}
.search-field {
padding-left: 40px;
padding-right: 40px;
}
.search-clear {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
width: 32px;
height: 32px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.search-input:focus-within .search-clear,
.search-field:not(:placeholder-shown) + .search-clear {
opacity: 1;
pointer-events: auto;
}
M4: Form Field
Purpose: Label + input + error message group
Test Criteria:
composition:
- atom: label
- atom: input
- atom: error_text (conditional)
behavior:
- label_for_input: id matching
- error_state: validation
- required_indicator: asterisk
accessibility:
- label_visible: true
- aria_describedby: error id
- aria_required: true if required
Code:
<div class="form-field" data-testid="form-field">
<label for="email-input" class="form-label">
Email Address
<span class="form-required" aria-label="required">*</span>
</label>
<input
id="email-input"
type="email"
class="input"
aria-required="true"
aria-describedby="email-error"
/>
<span id="email-error" class="form-error">
Please enter a valid email address
</span>
</div>
CSS:
.form-field {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--gray-700);
margin-bottom: 6px;
}
.form-required {
color: var(--red-500);
margin-left: 2px;
}
.form-error {
display: block;
margin-top: 6px;
font-size: 12px;
color: var(--red-500);
}
.form-field:not(.has-error) .form-error {
display: none;
}
M5: Progress Indicator
Purpose: Progress bar with percentage label
Test Criteria:
composition:
- atom: progress_bar
- atom: text_label
behavior:
- percentage_sync: bar and text match
- color_threshold: changes at limits
- animation: smooth transition
accessibility:
- aria_valuenow: numeric value
- aria_label: context description
Code:
<div class="progress-indicator" data-testid="progress-indicator">
<div class="progress-header">
<span class="progress-title">Project Progress</span>
<span class="progress-value">65%</span>
</div>
<div class="progress-bar" role="progressbar" aria-valuenow="65">
<div class="progress-fill" style="width: 65%;"></div>
</div>
<span class="progress-detail">8 of 12 tasks complete</span>
</div>
CSS:
.progress-indicator {
width: 100%;
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.progress-title {
font-size: 14px;
font-weight: 500;
color: var(--gray-900);
}
.progress-value {
font-size: 14px;
font-weight: 600;
color: var(--gray-900);
}
.progress-detail {
display: block;
margin-top: 6px;
font-size: 12px;
color: var(--gray-600);
}
M6: Action Button Group
Purpose: Related actions grouped together
Test Criteria:
composition:
- atoms: multiple_buttons
- layout: horizontal_row
behavior:
- primary_action: rightmost
- secondary_action: leftmost
- spacing: 12px gap
accessibility:
- keyboard_navigation: tab between
- focus_order: left to right
Code:
<div class="button-group" data-testid="button-group">
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-secondary">Save Draft</button>
<button class="btn btn-primary">Publish</button>
</div>
CSS:
.button-group {
display: flex;
gap: 12px;
justify-content: flex-end;
}
/* Responsive: Stack on mobile */
@media (max-width: 640px) {
.button-group {
flex-direction: column-reverse;
}
.button-group .btn {
width: 100%;
}
}
ORGANISMS
O1: Card
Purpose: Container for related content
Test Criteria:
composition:
- layout: structured_sections
- molecules: multiple_components
behavior:
- clickable: entire card (optional)
- hover_state: elevation change
- focus_state: outline visible
accessibility:
- semantic_html: article/section
- heading_structure: proper levels
- link_area: if clickable
Code:
<article class="card" data-testid="card">
<!-- Card Header -->
<div class="card-header">
<div class="card-icon">🏢</div>
<div class="card-title-group">
<h3 class="card-title">Acme Corporation</h3>
<div class="status-indicator">
<div class="status-dot status-success"></div>
<span class="status-label">Active</span>
</div>
</div>
</div>
<!-- Card Content -->
<div class="card-content">
<div class="card-meta">
<span>42 active users</span>
<span class="meta-separator">•</span>
<span>8 projects</span>
</div>
</div>
<!-- Card Footer -->
<div class="card-footer">
<div class="progress-indicator">
<div class="progress-bar">
<div class="progress-fill" style="width: 85%;"></div>
</div>
<span class="progress-label">85%</span>
</div>
</div>
</article>
CSS:
.card {
background: var(--white);
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
transition: box-shadow 0.2s, transform 0.2s;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
.card-clickable {
cursor: pointer;
}
.card-clickable:hover {
transform: translateY(-2px);
}
.card-header {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 16px;
}
.card-icon {
font-size: 32px;
line-height: 1;
}
.card-title-group {
flex: 1;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: var(--gray-900);
margin: 0 0 6px 0;
}
.card-content {
margin-bottom: 16px;
}
.card-meta {
font-size: 14px;
color: var(--gray-600);
display: flex;
align-items: center;
gap: 8px;
}
.meta-separator {
color: var(--gray-300);
}
.card-footer {
padding-top: 16px;
border-top: 1px solid var(--gray-100);
}
Variants:
<!-- Compact Card -->
<div class="card card-compact">
<h3 class="card-title">Quick Info</h3>
<p>Minimal content card</p>
</div>
<!-- Featured Card (Highlighted) -->
<div class="card card-featured">
<span class="card-badge">Featured</span>
<!-- rest of card content -->
</div>
O2: Modal
Purpose: Overlay dialog for focused interaction
Test Criteria:
composition:
- overlay: backdrop
- container: dialog box
- molecules: header, content, actions
behavior:
- escape_closes: true
- click_outside_closes: true
- focus_trap: true
- scroll_lock_body: true
accessibility:
- role: dialog
- aria_modal: true
- aria_labelledby: title id
- focus_management: proper
Code:
<div class="modal-overlay"
data-testid="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title">
<div class="modal">
<!-- Modal Header -->
<div class="modal-header">
<h2 id="modal-title" class="modal-title">Create New Project</h2>
<button class="btn-icon modal-close" aria-label="Close dialog">
×
</button>
</div>
<!-- Modal Content -->
<div class="modal-content">
<div class="form-field">
<label for="project-name" class="form-label">Project Name</label>
<input id="project-name" type="text" class="input" />
</div>
<div class="form-field">
<label for="project-desc" class="form-label">Description</label>
<textarea id="project-desc" class="input"></textarea>
</div>
</div>
<!-- Modal Actions -->
<div class="modal-actions">
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-primary">Create Project</button>
</div>
</div>
</div>
CSS:
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.2s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal {
width: 90%;
max-width: 600px;
max-height: 90vh;
background: var(--white);
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
animation: slideUp 0.3s;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
padding: 24px 24px 16px;
border-bottom: 1px solid var(--gray-200);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: var(--gray-900);
margin: 0;
}
.modal-close {
width: 32px;
height: 32px;
font-size: 24px;
color: var(--gray-500);
}
.modal-content {
padding: 24px;
overflow-y: auto;
flex: 1;
}
.modal-actions {
padding: 16px 24px;
border-top: 1px solid var(--gray-200);
display: flex;
justify-content: flex-end;
gap: 12px;
}
JavaScript (Focus Management):
class Modal {
constructor(element) {
this.element = element;
this.focusableElements = null;
this.firstFocusable = null;
this.lastFocusable = null;
}
open() {
this.element.style.display = 'flex';
this.setupFocusTrap();
document.body.style.overflow = 'hidden';
// Focus first input or close button
setTimeout(() => {
const firstInput = this.element.querySelector('input, textarea');
if (firstInput) {
firstInput.focus();
} else {
this.firstFocusable.focus();
}
}, 100);
}
close() {
this.element.style.display = 'none';
document.body.style.overflow = '';
}
setupFocusTrap() {
this.focusableElements = this.element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
this.firstFocusable = this.focusableElements[0];
this.lastFocusable = this.focusableElements[this.focusableElements.length - 1];
this.element.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.close();
}
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === this.firstFocusable) {
e.preventDefault();
this.lastFocusable.focus();
}
} else {
if (document.activeElement === this.lastFocusable) {
e.preventDefault();
this.firstFocusable.focus();
}
}
}
});
// Close on backdrop click
this.element.addEventListener('click', (e) => {
if (e.target === this.element) {
this.close();
}
});
}
}
O3: Data Table
Purpose: Structured tabular data display
Test Criteria:
composition:
- structure: thead, tbody, tfoot
- molecules: sortable_headers, row_actions
behavior:
- sortable: click column header
- selectable: checkbox rows
- responsive: horizontal scroll
accessibility:
- role: table
- aria_sort: column direction
- aria_selected: row state
- caption: table description
Code:
<div class="table-container" data-testid="data-table">
<table class="table">
<caption class="sr-only">User List</caption>
<thead>
<tr>
<th>
<input type="checkbox" aria-label="Select all" />
</th>
<th>
<button class="table-sort" aria-sort="ascending">
Name
<span class="sort-icon">↑</span>
</button>
</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<input type="checkbox" aria-label="Select row" />
</td>
<td>
<div class="table-cell-user">
<div class="avatar avatar-initials" style="background: var(--blue-500);">SC</div>
<span>Sarah Chen</span>
</div>
</td>
<td>sarah@example.com</td>
<td>Admin</td>
<td>
<div class="status-indicator">
<div class="status-dot status-success"></div>
<span>Active</span>
</div>
</td>
<td>
<div class="table-actions">
<button class="btn-icon" aria-label="Edit">✏️</button>
<button class="btn-icon" aria-label="Delete">🗑️</button>
</div>
</td>
</tr>
<!-- More rows -->
</tbody>
</table>
</div>
CSS:
.table-container {
width: 100%;
overflow-x: auto;
border-radius: 8px;
border: 1px solid var(--gray-200);
}
.table {
width: 100%;
border-collapse: collapse;
background: var(--white);
}
.table thead {
background: var(--gray-50);
border-bottom: 1px solid var(--gray-200);
}
.table th {
padding: 12px 16px;
text-align: left;
font-size: 12px;
font-weight: 600;
color: var(--gray-700);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.table td {
padding: 12px 16px;
border-bottom: 1px solid var(--gray-100);
font-size: 14px;
color: var(--gray-900);
}
.table tbody tr:last-child td {
border-bottom: none;
}
.table tbody tr:hover {
background: var(--gray-50);
}
.table-sort {
display: flex;
align-items: center;
gap: 4px;
background: none;
border: none;
padding: 0;
font: inherit;
color: inherit;
cursor: pointer;
}
.sort-icon {
font-size: 10px;
opacity: 0.5;
}
.table-sort[aria-sort="ascending"] .sort-icon {
opacity: 1;
}
.table-cell-user {
display: flex;
align-items: center;
gap: 12px;
}
.table-actions {
display: flex;
gap: 4px;
}
/* Screen reader only caption */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
(Continuing in next file due to length...)