Accessibility
Accessibility features and best practices for FoundryKit primitives
Accessibility
FoundryKit primitives are built with accessibility as a first-class concern, leveraging Radix UI's robust accessibility features and following WCAG guidelines.
Built-in Accessibility
ARIA Attributes
All primitives include proper ARIA attributes out of the box:
import { Button, Input, Label } from '@foundrykit/primitives'
function AccessibleForm() {
return (
<form className="space-y-4">
<div>
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
type="email"
aria-describedby="email-help"
aria-required="true"
/>
<p id="email-help" className="text-sm text-muted-foreground">
We'll never share your email with anyone else.
</p>
</div>
<Button type="submit">Subscribe</Button>
</form>
)
}
Keyboard Navigation
All interactive components support full keyboard navigation:
import { Dialog, DialogContent, DialogTrigger, Button } from '@foundrykit/primitives'
function KeyboardAccessibleDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog (Press Enter or Space)</Button>
</DialogTrigger>
<DialogContent>
<p>This dialog can be navigated with Tab, Escape, and arrow keys.</p>
</DialogContent>
</Dialog>
)
}
Focus Management
Components automatically manage focus states and focus trapping:
import { Popover, PopoverContent, PopoverTrigger, Button } from '@foundrykit/primitives'
function FocusManagedPopover() {
return (
<Popover>
<PopoverTrigger asChild>
<Button>Open Popover</Button>
</PopoverTrigger>
<PopoverContent>
{/* Focus is automatically trapped within this content */}
<p>Focus is managed automatically</p>
<Button>Action</Button>
</PopoverContent>
</Popover>
)
}
Screen Reader Support
Semantic HTML
All components use semantic HTML elements:
import { Card, CardHeader, CardTitle, CardContent } from '@foundrykit/primitives'
function SemanticCard() {
return (
<Card>
<CardHeader>
<CardTitle>Article Title</CardTitle>
</CardHeader>
<CardContent>
<p>This content is properly structured for screen readers.</p>
</CardContent>
</Card>
)
}
Descriptive Labels
Always provide descriptive labels for form elements:
import { Input, Label, Textarea } from '@foundrykit/primitives'
function DescriptiveLabels() {
return (
<div className="space-y-4">
<div>
<Label htmlFor="username">Username (required)</Label>
<Input
id="username"
aria-describedby="username-requirements"
/>
<p id="username-requirements" className="text-sm text-muted-foreground">
Must be 3-20 characters long, letters and numbers only.
</p>
</div>
<div>
<Label htmlFor="bio">Biography</Label>
<Textarea
id="bio"
aria-describedby="bio-hint"
placeholder="Tell us about yourself..."
/>
<p id="bio-hint" className="text-sm text-muted-foreground">
Optional: Share your background and interests.
</p>
</div>
</div>
)
}
Color and Contrast
High Contrast Support
Components automatically support high contrast modes:
import { Button, Badge } from '@foundrykit/primitives'
function HighContrastSupport() {
return (
<div className="space-y-4">
{/* These components automatically adapt to high contrast mode */}
<Button variant="default">High Contrast Button</Button>
<Badge variant="secondary">High Contrast Badge</Badge>
</div>
)
}
Color Independence
Ensure information isn't conveyed by color alone:
import { Badge } from '@foundrykit/primitives'
function ColorIndependentInfo() {
return (
<div className="space-y-2">
{/* ✅ Good - Uses both color and text to convey status */}
<Badge variant="destructive" className="flex items-center gap-1">
<span aria-hidden="true">⚠️</span>
Error: Invalid input
</Badge>
<Badge variant="default" className="flex items-center gap-1">
<span aria-hidden="true">✅</span>
Success: Data saved
</Badge>
</div>
)
}
Motion and Animation
Reduced Motion Support
Respect user preferences for reduced motion:
import { Button } from '@foundrykit/primitives'
function ReducedMotionSupport() {
return (
<Button
className="transition-all duration-200 hover:scale-105"
style={{
// Respects prefers-reduced-motion
'@media (prefers-reduced-motion: reduce)': {
transition: 'none',
transform: 'none'
}
}}
>
Respects Motion Preferences
</Button>
)
}
Animation Accessibility
Ensure animations don't interfere with user experience:
import { Skeleton } from '@foundrykit/primitives'
function AccessibleLoading() {
return (
<div className="space-y-2">
{/* Skeleton animations are subtle and don't cause motion sickness */}
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
<Skeleton className="h-4 w-[300px]" />
</div>
)
}
Form Accessibility
Error Handling
Provide clear error messages and validation feedback:
import { Input, Label } from '@foundrykit/primitives'
function AccessibleFormValidation({ errors, touched }) {
return (
<div className="space-y-4">
<div>
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
type="email"
aria-invalid={errors.email && touched.email ? 'true' : 'false'}
aria-describedby={errors.email && touched.email ? 'email-error' : undefined}
/>
{errors.email && touched.email && (
<p id="email-error" className="text-sm text-destructive" role="alert">
{errors.email}
</p>
)}
</div>
</div>
)
}
Required Fields
Clearly indicate required fields:
import { Input, Label } from '@foundrykit/primitives'
function RequiredFields() {
return (
<div className="space-y-4">
<div>
<Label htmlFor="name">
Full Name <span aria-label="required" className="text-destructive">*</span>
</Label>
<Input
id="name"
aria-required="true"
required
/>
</div>
<div>
<Label htmlFor="company">Company (Optional)</Label>
<Input id="company" />
</div>
</div>
)
}
Interactive Components
Dialog Accessibility
Dialogs include proper focus management and ARIA attributes:
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, Button } from '@foundrykit/primitives'
function AccessibleDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open Accessible Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirmation Required</DialogTitle>
</DialogHeader>
<p>This dialog is fully accessible with keyboard navigation and screen readers.</p>
<div className="flex justify-end space-x-2">
<Button variant="outline">Cancel</Button>
<Button>Confirm</Button>
</div>
</DialogContent>
</Dialog>
)
}
Tab Navigation
Tabs include proper ARIA attributes and keyboard navigation:
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@foundrykit/primitives'
function AccessibleTabs() {
return (
<Tabs defaultValue="account" className="w-[400px]">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
<TabsTrigger value="notifications">Notifications</TabsTrigger>
</TabsList>
<TabsContent value="account">
Account settings content
</TabsContent>
<TabsContent value="password">
Password settings content
</TabsContent>
<TabsContent value="notifications">
Notification settings content
</TabsContent>
</Tabs>
)
}
Testing Accessibility
Manual Testing
Test your components with:
- Keyboard navigation: Tab, Enter, Space, Escape, arrow keys
- Screen readers: NVDA, JAWS, VoiceOver, TalkBack
- High contrast mode: Windows and macOS high contrast settings
- Zoom: Test at 200% zoom level
Automated Testing
Use accessibility testing tools:
// Example with @testing-library/jest-dom
import { render, screen } from '@testing-library/react'
import { Button } from '@foundrykit/primitives'
test('button is accessible', () => {
render(<Button>Click me</Button>)
const button = screen.getByRole('button', { name: /click me/i })
expect(button).toBeInTheDocument()
expect(button).toHaveAttribute('type', 'button')
})
Common Issues to Avoid
- Missing labels: Always provide labels for form controls
- Poor contrast: Ensure sufficient color contrast
- Keyboard traps: Don't trap users in interactive elements
- Motion sickness: Respect reduced motion preferences
- Screen reader confusion: Use semantic HTML and proper ARIA
Best Practices
1. Use Semantic HTML
// ✅ Good - Uses semantic button
<Button type="submit">Submit Form</Button>
// ❌ Avoid - Uses div as button
<div role="button" tabIndex={0}>Submit Form</div>
2. Provide Clear Labels
// ✅ Good - Clear, descriptive label
<Label htmlFor="email">Email Address (required)</Label>
// ❌ Avoid - Vague label
<Label htmlFor="email">Email</Label>
3. Handle Errors Gracefully
// ✅ Good - Clear error message with ARIA
<Input
aria-invalid="true"
aria-describedby="email-error"
/>
<p id="email-error" role="alert">Please enter a valid email address</p>
// ❌ Avoid - No error feedback
<Input />
4. Test with Real Users
Always test your components with users who rely on assistive technologies to ensure they work as expected.
Next Steps
- Review styling guidelines to ensure accessible styling
- Learn about component composition for building accessible patterns
- Check best practices for optimal accessibility