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> with htmlFor attribute
  • • Use aria-describedby to link inputs with helper text
  • • Use aria-invalid and aria-errormessage for errors
  • • Mark required fields with both visual indicator and required attribute

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>
  );
}

Keyboard navigation

Button keyboard support

Buttons respond to Enter and Space keys automatically:

// All UI Lab buttons support keyboard activation
<Button onClick={() => console.log('Clicked')}>
  Click me (or press Enter/Space)
</Button>

Tab order

Maintain logical tab order by following the visual flow of your layout. Use tabIndex carefully, preferably only when necessary:

{/* Good: Tab order follows visual flow left-to-right, top-to-bottom */}
<Button>First</Button>
<Button>Second</Button>
<Button>Third</Button>

{/* Avoid: Explicit tabIndex unless absolutely necessary */}
{/* <Button tabIndex={2}>Out of order</Button> */}

Skip links

For complex layouts, provide skip links to jump past navigation:

export default function Layout() {
  return (
    <>
      <a href="#main" className="sr-only focus:not-sr-only">
        Skip to main content
      </a>

      <nav>Navigation here</nav>

      <main id="main">Main content</main>
    </>
  );
}

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