Accessibility
UI Lab components are built with accessibility first. Learn what's included and how to ensure your implementations remain accessible.
Built-in accessibility features
All UI Lab components are built to meet WCAG 2.1 Level AA standards by default. This includes:
Semantic HTML
Components use semantic HTML elements (<button>, <input>, <label>, etc) instead of divs with custom behaviors. This ensures assistive technologies understand component purpose.
Keyboard navigation
All interactive components are fully keyboard accessible. Use Tab to navigate, Enter/Space to activate, Arrow keys for navigation in lists.
ARIA attributes
Components automatically set appropriate ARIA attributes. For example, Button includes aria-disabled when disabled, and Input sets aria-invalid when in an error state.
Color contrast
All text and interactive elements meet WCAG AA contrast requirements (4.5:1 for text, 3:1 for large text and UI components).
Focus management
Components maintain visible focus indicators for keyboard navigation. The focus ring is always visible and has sufficient contrast.
Screen reader support
Components provide appropriate labels and announcements for screen readers. Buttons announce their purpose, form fields announce labels and error states.
Forms and labels
Always associate labels with inputs using the htmlFor attribute:
import { Input, Label } from '@ui-lab/core';
export default function LoginForm() {
return (
<div className="space-y-4">
<div>
<Label htmlFor="email">Email address</Label>
<Input
id="email"
type="email"
aria-describedby="email-hint"
/>
<p id="email-hint" className="text-sm text-foreground-400 mt-1">
We'll never share your email.
</p>
</div>
<div>
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" />
</div>
</div>
);
}Key points for accessible forms:
- • Always use
<Label>withhtmlForattribute - • Use
aria-describedbyto link inputs with helper text - • Use
aria-invalidandaria-errormessagefor errors - • Mark required fields with both visual indicator and
requiredattribute
Error handling with accessibility
import { useState } from 'react';
import { Input, Label } from '@ui-lab/core';
export default function EmailInput() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const value = e.target.value;
setEmail(value);
if (value && !value.includes('@')) {
setError('Please enter a valid email');
} else {
setError('');
}
};
return (
<div>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={email}
onChange={handleChange}
aria-invalid={!!error}
aria-describedby={error ? 'email-error' : undefined}
/>
{error && (
<p id="email-error" className="text-destructive-500 text-sm mt-1">
{error}
</p>
)}
</div>
);
}Adding custom ARIA attributes
UI Lab components support standard ARIA attributes. Pass them through like any HTML attribute:
{/* Provide context with ARIA attributes */}
<Button
aria-label="Delete item"
aria-describedby="delete-warning"
>
🗑
</Button>
<p id="delete-warning">This action cannot be undone</p>
{/* Mark components as busy during async operations */}
<Button aria-busy={isLoading}>
{isLoading ? 'Loading...' : 'Submit'}
</Button>
{/* Announce live region updates */}
<div aria-live="polite" aria-atomic="true">
{message}
</div>Common ARIA attributes:
- •
aria-label— Accessible name when label text isn't appropriate - •
aria-describedby— Links to descriptive text by ID - •
aria-invalid— Indicates validation errors - •
aria-busy— Indicates loading or processing state - •
aria-live— Announces dynamic content to screen readers - •
aria-expanded— Indicates if collapsible content is open
Testing accessibility
Manual testing
Test keyboard navigation yourself:
- • Use Tab key to navigate all interactive elements
- • Verify focus indicator is always visible
- • Test all interactions using keyboard only (no mouse)
- • Check that focus doesn't jump unexpectedly
- • Verify form labels are properly associated
Automated testing tools
Use browser tools to check accessibility:
- • axe DevTools — Browser extension for accessibility audits
- • Lighthouse — Chrome DevTools built-in accessibility checks
- • WAVE — Browser extension to visualize accessibility issues
- • Pa11y — Command-line accessibility testing
- • Jest and @testing-library/a11y — Automated testing in your test suite
Screen reader testing
Test with actual screen readers on your platform:
- • macOS — VoiceOver (built-in, Cmd+F5)
- • Windows — NVDA (free) or JAWS (paid)
- • iOS — VoiceOver (built-in, Settings → Accessibility)
- • Android — TalkBack (built-in, Settings → Accessibility)
Common accessibility mistakes to avoid
Using divs for buttons
Always use the Button component instead of styling divs as buttons:
// ❌ Don't do this
<div onClick={handleClick} className="bg-blue-500 p-2 rounded">
Click me
</div>
// ✅ Do this
<Button onClick={handleClick}>
Click me
</Button>Missing form labels
Always use Label components with proper associations:
// ❌ Don't do this
<Input placeholder="Email" />
// ✅ Do this
<Label htmlFor="email">Email</Label>
<Input id="email" placeholder="you@example.com" />Relying only on color for meaning
Don't use color alone to communicate important information:
// ❌ Don't do this
<p className="text-red-500">Error</p>
// ✅ Do this
<p className="text-destructive-500">
⚠️ Error: Please check your input
</p>Poor focus management
Don't hide focus indicators with outline: none or insufficient contrast:
// ❌ Don't do this
button {
outline: none; /* Removes default focus indicator! */
}
// ✅ Do this (UI Lab does this by default)
button:focus {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
}Resources
- • WCAG 2.1 Quick Reference — Official web content accessibility guidelines
- • WAI Web Accessibility Tutorials — Step-by-step accessibility guides
- • A11y 101 — Accessibility basics and best practices
- • Deque University — Comprehensive accessibility training