Accessibility
Accessibility features and guidelines for building inclusive interfaces.
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