CODITECT Pattern Library - Missing Atoms (Complete Specifications)
Completing the remaining 5% - Production specifications
A8: Label
Purpose: Form field labels and descriptive text
Test Criteria:
visual:
- font_size: 14px
- font_weight: 500
- color: gray-700
- margin_bottom: 6px
semantic:
- for_attribute: matches input id
- required_indicator: asterisk (red)
- optional_indicator: text (gray)
accessibility:
- associated_input: via for attribute
- required_clarity: visual + aria-required
- color_not_only: text + symbol
Variants:
<!-- Standard Label -->
<label for="email" class="label" data-testid="label-standard">
Email Address
</label>
<!-- Required Field -->
<label for="username" class="label" data-testid="label-required">
Username
<span class="label__required" aria-label="required">*</span>
</label>
<!-- Optional Field -->
<label for="company" class="label" data-testid="label-optional">
Company Name
<span class="label__optional">(optional)</span>
</label>
<!-- With Help Text -->
<label for="password" class="label" data-testid="label-with-help">
Password
<span class="label__help">Must be at least 8 characters</span>
</label>
<!-- Error State -->
<label for="email-error" class="label label--error" data-testid="label-error">
Email Address
<span class="label__error-icon" aria-hidden="true">⚠</span>
</label>
<!-- Disabled State -->
<label for="disabled-input" class="label label--disabled" data-testid="label-disabled">
Disabled Field
</label>
CSS:
.label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--gray-700);
margin-bottom: 6px;
line-height: 1.5;
}
.label__required {
color: var(--red-500);
margin-left: 2px;
font-weight: 600;
}
.label__optional {
color: var(--gray-500);
font-weight: 400;
margin-left: 4px;
}
.label__help {
display: block;
font-size: 12px;
font-weight: 400;
color: var(--gray-600);
margin-top: 2px;
}
.label--error {
color: var(--red-600);
}
.label__error-icon {
margin-left: 4px;
color: var(--red-500);
}
.label--disabled {
color: var(--gray-400);
cursor: not-allowed;
}
Test Cases:
test('label is associated with input', () => {
const label = document.querySelector('[for="email"]');
const input = document.querySelector('#email');
expect(label.htmlFor).toBe(input.id);
});
test('required indicator is visible', () => {
const required = document.querySelector('.label__required');
expect(required).toHaveTextContent('*');
expect(required).toHaveAttribute('aria-label', 'required');
});
test('help text has correct styling', () => {
const help = document.querySelector('.label__help');
const styles = getComputedStyle(help);
expect(styles.fontSize).toBe('12px');
expect(styles.fontWeight).toBe('400');
});
A9: Icon
Purpose: Visual symbols and indicators
Test Criteria:
visual:
- size: 16px (default), 12px (sm), 20px (lg), 24px (xl)
- color: currentColor (inherits)
- stroke_width: 2px (default)
behavior:
- interactive: cursor pointer + hover
- decorative: aria-hidden true
- semantic: aria-label present
accessibility:
- role: img (if semantic)
- aria_label: description (if semantic)
- aria_hidden: true (if decorative)
Variants:
<!-- Decorative Icon (no semantic meaning) -->
<svg
class="icon"
width="16"
height="16"
aria-hidden="true"
data-testid="icon-decorative"
>
<path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="2"/>
</svg>
<!-- Semantic Icon (conveys meaning) -->
<svg
class="icon"
width="16"
height="16"
role="img"
aria-label="Add new item"
data-testid="icon-semantic"
>
<path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="2"/>
</svg>
<!-- Interactive Icon (clickable) -->
<button class="icon-button" aria-label="Close dialog" data-testid="icon-button">
<svg class="icon" width="16" height="16">
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<!-- Size Variants -->
<svg class="icon icon--sm" width="12" height="12">...</svg>
<svg class="icon icon--md" width="16" height="16">...</svg>
<svg class="icon icon--lg" width="20" height="20">...</svg>
<svg class="icon icon--xl" width="24" height="24">...</svg>
<!-- Color Variants -->
<svg class="icon icon--primary" width="16" height="16">...</svg>
<svg class="icon icon--success" width="16" height="16">...</svg>
<svg class="icon icon--warning" width="16" height="16">...</svg>
<svg class="icon icon--error" width="16" height="16">...</svg>
CSS:
.icon {
display: inline-block;
width: 16px;
height: 16px;
flex-shrink: 0;
color: currentColor;
vertical-align: middle;
}
/* Size variants */
.icon--sm {
width: 12px;
height: 12px;
}
.icon--lg {
width: 20px;
height: 20px;
}
.icon--xl {
width: 24px;
height: 24px;
}
/* Color variants */
.icon--primary {
color: var(--blue-500);
}
.icon--success {
color: var(--green-500);
}
.icon--warning {
color: var(--yellow-500);
}
.icon--error {
color: var(--red-500);
}
.icon--muted {
color: var(--gray-400);
}
/* Interactive wrapper */
.icon-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
border: none;
background: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
}
.icon-button:hover {
background: var(--gray-100);
}
.icon-button:focus-visible {
outline: 2px solid var(--blue-500);
outline-offset: 2px;
}
Common Icons (SVG paths):
<!-- Plus -->
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<!-- X / Close -->
<path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<!-- Check -->
<path d="M20 6L9 17l-5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Chevron Down -->
<path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Search -->
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2" fill="none"/>
<path d="M21 21l-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<!-- Settings -->
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" fill="none"/>
<path d="M12 1v6m0 6v6M23 12h-6m-6 0H5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<!-- User -->
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2" fill="none"/>
<!-- Menu / Hamburger -->
<path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
A10: Checkbox
Purpose: Multiple selection control
Test Criteria:
visual:
- size: 20px × 20px (44px touch target)
- border: 2px solid
- border_radius: 4px
- checkmark: visible when checked
behavior:
- toggle: click to check/uncheck
- keyboard: space to toggle
- indeterminate: partial selection state
accessibility:
- role: checkbox
- aria_checked: true/false/mixed
- label_associated: via id or wrapping
- focus_visible: outline
Variants:
<!-- Unchecked -->
<label class="checkbox" data-testid="checkbox-unchecked">
<input type="checkbox" class="checkbox__input" />
<span class="checkbox__box"></span>
<span class="checkbox__label">Option 1</span>
</label>
<!-- Checked -->
<label class="checkbox" data-testid="checkbox-checked">
<input type="checkbox" class="checkbox__input" checked />
<span class="checkbox__box">
<svg class="checkbox__check" width="12" height="12" viewBox="0 0 12 12">
<path d="M2 6l3 3 5-6" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
</span>
<span class="checkbox__label">Option 2</span>
</label>
<!-- Indeterminate (partial selection) -->
<label class="checkbox" data-testid="checkbox-indeterminate">
<input type="checkbox" class="checkbox__input" />
<span class="checkbox__box checkbox__box--indeterminate">
<svg width="12" height="12" viewBox="0 0 12 12">
<path d="M2 6h8" stroke="currentColor" stroke-width="2"/>
</svg>
</span>
<span class="checkbox__label">Select All</span>
</label>
<!-- Disabled -->
<label class="checkbox checkbox--disabled" data-testid="checkbox-disabled">
<input type="checkbox" class="checkbox__input" disabled />
<span class="checkbox__box"></span>
<span class="checkbox__label">Disabled Option</span>
</label>
<!-- Error State -->
<label class="checkbox checkbox--error" data-testid="checkbox-error">
<input type="checkbox" class="checkbox__input" aria-invalid="true" />
<span class="checkbox__box"></span>
<span class="checkbox__label">Required Option</span>
</label>
CSS:
.checkbox {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
position: relative;
padding: 12px 0; /* Expand touch target */
}
.checkbox__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.checkbox__box {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: 2px solid var(--gray-300);
border-radius: 4px;
background: var(--white);
transition: all 0.2s;
flex-shrink: 0;
}
.checkbox__input:checked + .checkbox__box {
background: var(--blue-500);
border-color: var(--blue-500);
}
.checkbox__check {
color: var(--white);
}
.checkbox__box--indeterminate {
background: var(--blue-500);
border-color: var(--blue-500);
}
.checkbox__label {
font-size: 14px;
color: var(--gray-900);
line-height: 1.5;
}
/* States */
.checkbox:hover .checkbox__box {
border-color: var(--blue-500);
}
.checkbox__input:focus-visible + .checkbox__box {
outline: 2px solid var(--blue-500);
outline-offset: 2px;
}
.checkbox--disabled {
cursor: not-allowed;
opacity: 0.5;
}
.checkbox--error .checkbox__box {
border-color: var(--red-500);
}
.checkbox--error .checkbox__input:checked + .checkbox__box {
background: var(--red-500);
border-color: var(--red-500);
}
JavaScript (for indeterminate):
// Set indeterminate state (cannot be set via HTML)
const checkbox = document.querySelector('input[type="checkbox"]');
checkbox.indeterminate = true;
// Parent checkbox controls children
const selectAll = document.querySelector('#select-all');
const children = document.querySelectorAll('.child-checkbox');
selectAll.addEventListener('change', (e) => {
children.forEach(child => {
child.checked = e.target.checked;
});
});
// Update parent based on children
children.forEach(child => {
child.addEventListener('change', () => {
const checkedCount = Array.from(children).filter(c => c.checked).length;
if (checkedCount === 0) {
selectAll.checked = false;
selectAll.indeterminate = false;
} else if (checkedCount === children.length) {
selectAll.checked = true;
selectAll.indeterminate = false;
} else {
selectAll.checked = false;
selectAll.indeterminate = true;
}
});
});
A11: Radio
Purpose: Single selection from options
Test Criteria:
visual:
- size: 20px × 20px (44px touch target)
- shape: circle (border-radius 50%)
- border: 2px solid
- selected: inner dot
behavior:
- mutually_exclusive: only one selected per group
- keyboard: arrow keys navigate group
- name_attribute: groups radios together
accessibility:
- role: radio (implicit)
- aria_checked: true/false
- label_associated: via id or wrapping
- keyboard_navigation: arrow keys
Variants:
<!-- Radio Group -->
<fieldset class="radio-group" data-testid="radio-group">
<legend class="radio-group__legend">Choose your plan</legend>
<!-- Unselected -->
<label class="radio" data-testid="radio-unselected">
<input type="radio" name="plan" value="free" class="radio__input" />
<span class="radio__circle"></span>
<span class="radio__label">
<span class="radio__title">Free Plan</span>
<span class="radio__description">Basic features</span>
</span>
</label>
<!-- Selected -->
<label class="radio" data-testid="radio-selected">
<input type="radio" name="plan" value="pro" class="radio__input" checked />
<span class="radio__circle">
<span class="radio__dot"></span>
</span>
<span class="radio__label">
<span class="radio__title">Pro Plan</span>
<span class="radio__description">Advanced features</span>
</span>
</label>
<!-- Disabled -->
<label class="radio radio--disabled" data-testid="radio-disabled">
<input type="radio" name="plan" value="enterprise" class="radio__input" disabled />
<span class="radio__circle"></span>
<span class="radio__label">
<span class="radio__title">Enterprise</span>
<span class="radio__description">Contact sales</span>
</span>
</label>
</fieldset>
<!-- Compact Radio (no description) -->
<label class="radio radio--compact">
<input type="radio" name="size" value="small" class="radio__input" />
<span class="radio__circle"></span>
<span class="radio__label">Small</span>
</label>
CSS:
.radio-group {
border: none;
padding: 0;
margin: 0;
}
.radio-group__legend {
font-size: 14px;
font-weight: 500;
color: var(--gray-700);
margin-bottom: 12px;
padding: 0;
}
.radio {
display: flex;
align-items: flex-start;
gap: 12px;
cursor: pointer;
padding: 12px;
border-radius: 8px;
transition: background-color 0.2s;
margin-bottom: 8px;
}
.radio:hover {
background: var(--gray-50);
}
.radio__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.radio__circle {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: 2px solid var(--gray-300);
border-radius: 50%;
background: var(--white);
transition: all 0.2s;
flex-shrink: 0;
margin-top: 2px; /* Align with text baseline */
}
.radio__input:checked + .radio__circle {
border-color: var(--blue-500);
}
.radio__dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--blue-500);
}
.radio__label {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
.radio__title {
font-size: 14px;
font-weight: 500;
color: var(--gray-900);
line-height: 1.5;
}
.radio__description {
font-size: 12px;
color: var(--gray-600);
line-height: 1.4;
}
/* States */
.radio__input:focus-visible + .radio__circle {
outline: 2px solid var(--blue-500);
outline-offset: 2px;
}
.radio--disabled {
cursor: not-allowed;
opacity: 0.5;
}
.radio--disabled:hover {
background: none;
}
/* Compact variant */
.radio--compact {
padding: 8px;
align-items: center;
}
.radio--compact .radio__circle {
margin-top: 0;
}
A12: Toggle (Switch)
Purpose: On/off control for settings
Test Criteria:
visual:
- width: 44px, height: 24px
- border_radius: 12px (pill shape)
- thumb: 20px circle
- transition: smooth slide (0.2s)
behavior:
- toggle: click to switch
- keyboard: space to toggle
- touch_friendly: 44px minimum
accessibility:
- role: switch
- aria_checked: true/false
- label_associated: via id
- keyboard_operable: space/enter
Variants:
<!-- Off State -->
<label class="toggle" data-testid="toggle-off">
<input type="checkbox" class="toggle__input" role="switch" />
<span class="toggle__track">
<span class="toggle__thumb"></span>
</span>
<span class="toggle__label">Email notifications</span>
</label>
<!-- On State -->
<label class="toggle" data-testid="toggle-on">
<input type="checkbox" class="toggle__input" role="switch" checked />
<span class="toggle__track toggle__track--checked">
<span class="toggle__thumb"></span>
</span>
<span class="toggle__label">Email notifications</span>
</label>
<!-- With Description -->
<label class="toggle" data-testid="toggle-with-description">
<input type="checkbox" class="toggle__input" role="switch" />
<span class="toggle__track">
<span class="toggle__thumb"></span>
</span>
<span class="toggle__content">
<span class="toggle__label">Dark mode</span>
<span class="toggle__description">Use dark theme throughout the app</span>
</span>
</label>
<!-- Disabled -->
<label class="toggle toggle--disabled" data-testid="toggle-disabled">
<input type="checkbox" class="toggle__input" role="switch" disabled />
<span class="toggle__track">
<span class="toggle__thumb"></span>
</span>
<span class="toggle__label">Disabled setting</span>
</label>
<!-- Small Size -->
<label class="toggle toggle--sm">
<input type="checkbox" class="toggle__input" role="switch" />
<span class="toggle__track">
<span class="toggle__thumb"></span>
</span>
<span class="toggle__label">Compact toggle</span>
</label>
CSS:
.toggle {
display: inline-flex;
align-items: flex-start;
gap: 12px;
cursor: pointer;
user-select: none;
}
.toggle__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.toggle__track {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
background: var(--gray-300);
border-radius: 12px;
transition: background-color 0.2s;
flex-shrink: 0;
}
.toggle__track--checked {
background: var(--blue-500);
}
.toggle__thumb {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: var(--white);
border-radius: 50%;
transition: transform 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.toggle__input:checked + .toggle__track .toggle__thumb {
transform: translateX(20px);
}
.toggle__content {
display: flex;
flex-direction: column;
gap: 2px;
}
.toggle__label {
font-size: 14px;
font-weight: 500;
color: var(--gray-900);
line-height: 1.5;
}
.toggle__description {
font-size: 12px;
color: var(--gray-600);
line-height: 1.4;
}
/* States */
.toggle:hover .toggle__track {
background: var(--gray-400);
}
.toggle:hover .toggle__track--checked {
background: var(--blue-600);
}
.toggle__input:focus-visible + .toggle__track {
outline: 2px solid var(--blue-500);
outline-offset: 2px;
}
.toggle--disabled {
cursor: not-allowed;
opacity: 0.5;
}
.toggle--disabled:hover .toggle__track {
background: var(--gray-300);
}
/* Size variant */
.toggle--sm .toggle__track {
width: 36px;
height: 20px;
border-radius: 10px;
}
.toggle--sm .toggle__thumb {
width: 16px;
height: 16px;
}
.toggle--sm .toggle__input:checked + .toggle__track .toggle__thumb {
transform: translateX(16px);
}
A13: Divider
Purpose: Visual content separator
Test Criteria:
visual:
- height: 1px (horizontal)
- width: 1px (vertical)
- color: gray-200
- margin: 16px or 24px
semantic:
- role: separator
- aria_orientation: horizontal/vertical
- decorative: aria-hidden if purely visual
accessibility:
- not_keyboard_focusable: true
- role: separator
Variants:
<!-- Horizontal Divider -->
<hr class="divider" role="separator" data-testid="divider-horizontal" />
<!-- With Text -->
<div class="divider divider--text" role="separator" data-testid="divider-text">
<span class="divider__text">or</span>
</div>
<!-- Vertical Divider -->
<div class="divider divider--vertical" role="separator" aria-orientation="vertical" data-testid="divider-vertical"></div>
<!-- Thick Variant -->
<hr class="divider divider--thick" role="separator" />
<!-- Dashed Variant -->
<hr class="divider divider--dashed" role="separator" />
<!-- Spacing Variants -->
<hr class="divider divider--sm" role="separator" /> <!-- 8px margin -->
<hr class="divider" role="separator" /> <!-- 16px margin (default) -->
<hr class="divider divider--lg" role="separator" /> <!-- 24px margin -->
CSS:
.divider {
width: 100%;
height: 1px;
background: var(--gray-200);
border: none;
margin: 16px 0;
}
/* With text */
.divider--text {
display: flex;
align-items: center;
gap: 16px;
height: auto;
background: none;
margin: 24px 0;
}
.divider--text::before,
.divider--text::after {
content: '';
flex: 1;
height: 1px;
background: var(--gray-200);
}
.divider__text {
font-size: 12px;
font-weight: 500;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
/* Vertical */
.divider--vertical {
width: 1px;
height: 100%;
background: var(--gray-200);
margin: 0 16px;
display: inline-block;
}
/* Thickness variants */
.divider--thick {
height: 2px;
}
/* Style variants */
.divider--dashed {
background: none;
border-top: 1px dashed var(--gray-300);
height: 0;
}
/* Spacing variants */
.divider--sm {
margin: 8px 0;
}
.divider--lg {
margin: 24px 0;
}
.divider--xl {
margin: 32px 0;
}
Usage Examples:
<!-- In a list -->
<ul>
<li>Item 1</li>
<hr class="divider" />
<li>Item 2</li>
<hr class="divider" />
<li>Item 3</li>
</ul>
<!-- Between sections -->
<section>
<h2>Section 1</h2>
<p>Content...</p>
</section>
<hr class="divider divider--lg" />
<section>
<h2>Section 2</h2>
<p>Content...</p>
</section>
<!-- In navigation -->
<nav>
<a href="#home">Home</a>
<div class="divider divider--vertical"></div>
<a href="#about">About</a>
<div class="divider divider--vertical"></div>
<a href="#contact">Contact</a>
</nav>
<!-- With text (auth forms) -->
<form>
<button>Sign in with Google</button>
<div class="divider divider--text">
<span class="divider__text">or</span>
</div>
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
</form>
Test Suite for Missing Atoms
describe('Atom: Label', () => {
test('associates with input via for attribute', () => {
const label = document.querySelector('[for="email"]');
const input = document.getElementById('email');
expect(label.htmlFor).toBe(input.id);
});
test('required indicator is accessible', () => {
const required = document.querySelector('.label__required');
expect(required).toHaveAttribute('aria-label', 'required');
});
});
describe('Atom: Checkbox', () => {
test('toggles on click', () => {
const checkbox = document.querySelector('.checkbox__input');
expect(checkbox.checked).toBe(false);
checkbox.click();
expect(checkbox.checked).toBe(true);
});
test('supports indeterminate state', () => {
const checkbox = document.querySelector('.checkbox__input');
checkbox.indeterminate = true;
expect(checkbox.indeterminate).toBe(true);
});
});
describe('Atom: Radio', () => {
test('only one selected in group', () => {
const radios = document.querySelectorAll('[name="plan"]');
radios[0].click();
expect(radios[0].checked).toBe(true);
radios[1].click();
expect(radios[0].checked).toBe(false);
expect(radios[1].checked).toBe(true);
});
test('keyboard navigation with arrow keys', () => {
const radios = document.querySelectorAll('[name="plan"]');
radios[0].focus();
fireEvent.keyDown(radios[0], { key: 'ArrowDown' });
expect(document.activeElement).toBe(radios[1]);
});
});
describe('Atom: Toggle', () => {
test('has role switch', () => {
const toggle = document.querySelector('.toggle__input');
expect(toggle).toHaveAttribute('role', 'switch');
});
test('toggles with space key', () => {
const toggle = document.querySelector('.toggle__input');
toggle.focus();
fireEvent.keyDown(toggle, { key: ' ' });
expect(toggle.checked).toBe(true);
});
});
describe('Atom: Divider', () => {
test('has role separator', () => {
const divider = document.querySelector('.divider');
expect(divider).toHaveAttribute('role', 'separator');
});
test('vertical has aria-orientation', () => {
const divider = document.querySelector('.divider--vertical');
expect(divider).toHaveAttribute('aria-orientation', 'vertical');
});
});
This completes all 13 atoms with production-grade specifications, test criteria, variants, accessibility requirements, and working code examples.