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> 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