Accessibility
Native elements provide the baseline. Custom widgets follow WAI-ARIA Authoring Practices for semantics, names, keyboard behavior, and focus.
Commitments
- Native elements first:
<dialog>,<details>, the Popover API, real inputs. The browser supplies the semantics and the keyboard. - APG patterns where the platform has nothing: listbox, combobox, menu, menubar, grid, tabs, window splitter. Arrow keys, Home and End, type-ahead, focus returned to the trigger.
- A visible 2px focus ring on every control, in ink, with offset. Never removed.
- Control boundaries at 3:1 against the ground through
--control-border(WCAG 1.4.11). Hairlines stay decorative. - Text at 4.5:1 or better in both schemes. Emphasis by weight and size; nothing is carried by colour alone, because there is only one.
prefers-reduced-motion: transitions off, looping demos and unsolicited entry disabled.forced-colors: every control keeps a border and a state in system colours.- Hit targets of 40px on the desktop and 44px at or under 640px.
- Every chart carries a screen-reader table of its data and a keyboard cursor with a status tooltip. Numbers format through
Intl.NumberFormatwith an optionallocale. - Every interactive component has an axe pass and a keyboard test in the suite.
Naming things
// Controls without visible text take a name
<Switch aria-label="Notifications" />
<Slider aria-label="Volume" />
<Progress label="Uploading" value={40} />
<Spinner label="Loading" />
<LineChart aria-label="Sheets per day" … />
// Field wires label, hint, and error to the control
<Field>
<FieldLabel htmlFor="email">E-mail</FieldLabel>
<Input plain id="email" type="email" />
<FieldHint>We reply within a day.</FieldHint>
<FieldError>Enter a valid address.</FieldError>
</Field>
// Dialog titles are h2 and name the dialog; the close button gets a label
<Dialog open={open} onClose={close} closeLabel="Close">
<DialogTitle>Remove this item?</DialogTitle>
<DialogBody>This can't be undone.</DialogBody>
</Dialog>Components with visible text name themselves. Components without it (Switch, Slider, icon buttons, charts) accept aria-label or aria-labelledby and pass it to the element that carries the role. Field binds hint and error to its control with aria-describedby and sets aria-invalid while an error is mounted. Dialog, Sheet, and Drawer title themselves from their Title part and describe themselves from Body. Alert is a note by default; pass live only when it appears in response to something.
Testing
// vitest, jsdom
import { render } from "@testing-library/react";
import { axe } from "vitest-axe";
import { Select } from "@noorddev/vlak-react";
it("has no axe violations", async () => {
const { container } = render(<Select options={cities} aria-label="City" />);
expect(await axe(container)).toHaveNoViolations();
});The package suite runs axe on every interactive component and walks the keyboard path for each pattern: open, move, select, dismiss, focus back on the trigger. The same setup works in your project. Test your composition, not the component: the name you passed, the order of fields, the focus target after a dialog closes.
Patterns by component
| Component | Pattern | Keys |
|---|---|---|
| Button | Button, native <button> | Tab, Enter, Space |
| Button group | Group of native buttons | Tab, Enter, Space |
| Text link | Link, native <a> | Tab, Enter |
| Input | Native input | Tab, Enter |
| Field | Native label, hint, and error wired through aria-describedby | |
| Input group | Native input with a labelled addon | |
| Native select | Native <select> | Space, Alt + Arrow down, Arrow up, Arrow down, Type a letter |
| Inline form | Native form with one field | Enter, Tab |
| Radio | Radio group, native inputs | Tab, Arrow up, Arrow down, Arrow left, Arrow right, Space |
| Checkbox | Checkbox, native input | Tab, Space |
| Switch | Switch, role switch on a button | Tab, Space, Enter |
| Slider | Slider, native range input | Tab, Arrow right, Arrow up, Arrow left, Arrow down, Home, End |
| Progress | Progressbar | |
| Tabs | Tabs, horizontal or vertical | Tab, Arrow left, Arrow right, Arrow up, Arrow down, Home, End |
| Breadcrumbs | Breadcrumb, nav > ol with aria-current | Tab, Enter |
| Pagination | Navigation landmark with aria-current | Tab, Enter, Space |
| Select | Select-only combobox | Arrow down, Arrow up, Enter, Space, Home, End, Type letters, Arrow down, Arrow up, Page up, Page down, Enter, Space, Escape, Tab |
| Dialog | Dialog, native <dialog> | Escape, Tab, Shift + Tab, Enter, Space |
| Stepper | List with aria-current step | |
| Accordion | Disclosure, native <details> | Tab, Enter, Space |
| Alert | Note; status or alert when live | |
| Alert dialog | Alert dialog, native <dialog> with role alertdialog | Tab, Shift + Tab, Enter, Space, Escape |
| Textarea | Native textarea | Tab |
| Spinner | Status | |
| Tooltip | Tooltip | Tab, Escape |
| Toast | Status region, aria-live polite | Tab, Enter, Space |
| Dropdown menu | Menu button and menu | Arrow down, Enter, Space, Arrow up, Arrow down, Arrow up, Home, End, Type letters, Enter, Space, Escape, Tab |
| Toggle | Toggle button, aria-pressed | Tab, Enter, Space |
| Toggle group | Group of toggle buttons | Tab, Enter, Space |
| Popover | Popover API | Enter, Space, Escape, Tab |
| Sheet | Dialog, native <dialog> | Escape, Tab, Shift + Tab |
| Drawer | Dialog, native <dialog> | Escape, Tab, Shift + Tab |
| Scroll area | Region | Tab, Arrow up, Arrow down, Page up, Page down |
| Charts | Figure with a keyboard cursor and a screen-reader table | Tab, Arrow right, Arrow left, Home, End, Escape |
| Bar chart | Figure with a keyboard cursor and a screen-reader table | Tab, Arrow right, Arrow left, Home, End, Escape |
| Area chart | Figure with a keyboard cursor and a screen-reader table | Tab, Arrow right, Arrow left, Home, End, Escape |
| Scatter chart | Figure with a keyboard cursor and a screen-reader table | Tab, Arrow right, Arrow left, Home, End, Escape |
| Donut or share | Figure with a screen-reader table | |
| Histogram | Figure with a keyboard cursor and a screen-reader table | Tab, Arrow right, Arrow left, Home, End, Escape |
| Small multiples | Figures with screen-reader tables | Tab, Arrow right, Arrow left, Home, End, Escape |
| Collapsible | Disclosure, native <details> | Tab, Enter, Space |
| Hover card | Tooltip | Tab, Escape |
| One-time code | Group of native inputs | Type a digit, Backspace, Arrow left, Arrow right, Paste |
| Context menu | Menu, opened by pointer or Shift+F10 | Shift + F10, Context menu key, Arrow down, Arrow up, Home, End, Type letters, Enter, Space, Escape |
| Menubar | Menubar | Tab, Arrow left, Arrow right, Home, End, Arrow down, Enter, Space, Arrow up, Escape |
| Navigation menu | Navigation landmark | Tab, Enter |
| Sidebar | Navigation landmark | Tab, Enter |
| Carousel | Carousel, scroll snap with named groups | Tab, Arrow left, Arrow right, Enter, Space |
| Resizable | Window splitter, role separator | Tab, Arrow left, Arrow up, Arrow right, Arrow down, Shift + Arrow keys, Home, End |
| Combobox | Combobox with list autocomplete | Type, Arrow down, Arrow up, Home, End, Page up, Page down, Enter, Escape, Tab |
| Command | Dialog plus combobox and listbox | Type, Arrow down, Arrow up, Home, End, Page up, Page down, Enter, Escape |
| Calendar | Grid with a roving cell | Tab, Arrow left, Arrow right, Arrow up, Arrow down, Home, End, Page up, Page down, Shift + Page up, Shift + Page down, Enter, Space |
| Date picker | Dialog holding a grid | Enter, Space, Arrow down, Arrow keys, Home, End, Page up, Page down, Enter, Space, Escape, Tab |
| Data table | Table with sortable column headers (aria-sort) | Tab, Enter, Space |
| Form | Native form | Enter |
| Theme toggle | Button with a state-dependent name | Tab, Enter, Space |
Each component page carries the full keyboard table and the accessibility notes for that component.