Arcana UI

Accessibility

WCAG AA compliance, axe-core testing, keyboard navigation, and screen reader support in Arcana UI.

Our accessibility commitment

Every Arcana component is designed and tested to meet WCAG 2.1 AA standards. This means:

  • Minimum 4.5:1 contrast ratio for body text (3:1 for large text)
  • Full keyboard operability for all interactive elements
  • Semantic HTML with correct ARIA roles, states, and properties
  • Screen reader announcements for dynamic content
  • Focus management for modals and overlays
  • axe-core automated tests pass on all components (238 tests, all passing)

Testing with axe

All components are tested with jest-axe in the test suite:

import { axe } from 'jest-axe'
import { render } from '@testing-library/react'
import { Button } from '@arcana-ui/core'
 
it('passes axe checks', async () => {
  const { container } = render(<Button>Submit</Button>)
  const results = await axe(container)
  expect(results).toHaveNoViolations()
})

Keyboard navigation

ComponentKeys
ButtonEnter, Space to activate
Input, TextareaStandard text editing
SelectEnter/Space to open, Arrow keys to navigate, Enter to select, Escape to close
CheckboxSpace to toggle
RadioArrow keys to navigate group, Space to select
ToggleSpace, Enter to toggle
ModalEscape to close, Tab/Shift+Tab trapped inside
TabsArrow keys to switch tabs, Enter/Space to activate
AccordionEnter/Space to expand/collapse
Dropdown menusArrow keys, Enter, Escape

Focus management

The Modal component implements a complete focus trap: when open, Tab and Shift+Tab cycle only through focusable elements inside the dialog. Focus is restored to the triggering element when the modal closes.

<Modal
  open={isOpen}
  onClose={() => setIsOpen(false)}
  title="Confirm action"
>
  {/* Tab stays within this dialog */}
  <p>Are you sure?</p>
  <Button>Cancel</Button>
  <Button variant="danger">Delete</Button>
</Modal>

For page-level accessibility, add a skip link before your Navbar:

<a href="#main-content" className="sr-only focus:not-sr-only">
  Skip to main content
</a>
<Navbar />
<main id="main-content">
  {/* content */}
</main>

ARIA attributes

Required labels

All form components require accessible names:

{/* Good — label prop */}
<Input label="Email address" />
 
{/* Good — aria-label */}
<Input aria-label="Search" placeholder="Search..." />
 
{/* Bad — no accessible name */}
<Input placeholder="Email" />

Error states

Use the error prop on form components to associate error messages:

<Input
  label="Email"
  error="Please enter a valid email address"
  value={email}
  onChange={(e) => setEmail(e.target.value)}
/>

This adds aria-invalid="true" and aria-describedby pointing to the error message.

Icon-only buttons

Always add aria-label to buttons that contain only icons:

<Button aria-label="Close dialog">
  <XIcon />
</Button>

Colour contrast

Arcana's default palette is tested against WCAG AA:

TokenForegroundBackgroundRatioLevel
Primary text--arcana-text-primary--arcana-surface-default14.3:1AAA
Secondary text--arcana-text-secondary--arcana-surface-default6.2:1AA
Action text on primary--arcana-text-on-action--arcana-action-primary5.1:1AA
Placeholder--arcana-text-placeholder--arcana-surface-default4.6:1AA

When customising colours, use a contrast checker to verify:

Screen reader testing

Components are tested with:

  • macOS VoiceOver — Safari and Chrome
  • NVDA + Firefox (Windows)
  • axe DevTools browser extension

Live regions

Dynamic content (Toast notifications, form errors) uses appropriate ARIA live regions:

{/* Toast notifications use role="alert" */}
<Toast message="File saved successfully" variant="success" />
 
{/* Form errors use role="alert" */}
<Input label="Email" error="Invalid email" />

Reduced motion

Arcana respects prefers-reduced-motion:

@media (prefers-reduced-motion: reduce) {
  /* All animations are disabled automatically */
  --arcana-motion-duration-fast: 0ms;
  --arcana-motion-duration-normal: 0ms;
  --arcana-motion-duration-slow: 0ms;
}

Reporting accessibility issues

If you find an accessibility issue with any Arcana component, please open a GitHub issue with the component name, reproduction steps, and the assistive technology you tested with.

On this page