Accessibility
Native elements provide the baseline. Custom widgets follow WAI-ARIA Authoring Practices for semantics, names, keyboard behavior, and focus.
Submit an empty field to see its error announced and focus returned to the control.
Tab through the field and actions, then submit.
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, Arrow right, Arrow left, 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 |
| Calendar popover | Editable date field with a popover dialog and calendar grid | Tab, Enter, Space on the trigger, Arrow down in the field or on the trigger, Arrow keys, Home, End, Page up, Page down, Enter, Space on a day, Escape |
| 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.