FoundryKit

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

  1. Missing labels: Always provide labels for form controls
  2. Poor contrast: Ensure sufficient color contrast
  3. Keyboard traps: Don't trap users in interactive elements
  4. Motion sickness: Respect reduced motion preferences
  5. 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