Accessibility
Vamos UI targets WCAG 2.2 level AA. This page shows how that is tested, what each component does on the keyboard, and what is still open.
Target
Every component aims for WCAG 2.2 AA in both themes and both materials: the WAI-ARIA Authoring Practices pattern for its role, full keyboard operation, a visible focus ring (focus-ring), names for icon-only controls, 4.5:1 text contrast, 24 px minimum targets for pointer controls, logical properties for right-to-left layouts and no motion when the user asks for less. Interactive primitives are built on Radix, which handles focus management and ARIA wiring for overlays, menus and tabs.
How it is tested
Component tests (Vitest + axe)
Each component has a test file next to it, run with Vitest, Testing Library and vitest-axe in jsdom. The shared helper expectNoA11yViolations() renders the component inside the providers and fails on any axe violation. Interaction tests drive the real keyboard through user-event.
| Counted in packages/ui | Files | Calls |
|---|---|---|
| Test files | 205 | |
| axe assertions (expectNoA11yViolations / toHaveNoViolations) | 173 | 318 |
| Keyboard interactions (user.keyboard / user.tab) | 82 | 284 |
| Right-to-left renders (dir="rtl") | 5 | 7 |
Counted from the repository when this page was built. jsdom has no layout, so colour contrast is not part of the unit run; it is checked in the browser runs below.
Blocks, docs and example sites (Playwright + axe)
- 9 Playwright spec files run in Chrome; 8 of them call axe (
@axe-core/playwright) and fail on any serious or critical violation. - Every admin block and every page of every example site is checked at 1440 px and 390 px, together with one landmark, exactly one visible
h1on site pages, no console errors and no sideways scroll. - Docs component pages are smoke-tested across the theme matrix: light and dark, glass and solid.
- Example sites run axe with colour contrast on. Component and block pages disable the contrast rule, because preview stages put components on decorative backgrounds; contrast for tokens is checked by hand (see known issues).
- Signature interactions (menus, dialogs, sortable lists, language switch) are driven with the keyboard in the browser.
Reduced motion
Motion in the kit (@vamos-ui/react/motion, overlay and collapse transitions) renders its final state under prefers-reduced-motion: reduce or data-motion="off". 2 spec files run whole suites with Playwright's reducedMotion: "reduce" and assert that every heading and section is visible immediately, at 1440 and 390 px. A separate test keeps layout shift on the animated home pages under the 0.1 CLS budget.
Keyboard support
Every interactive component is operable with the keyboard alone. The table lists the components whose docs page documents keys beyond the platform defaults (Tab, Enter, Space on buttons and fields), taken from each page's notes. Buttons, inputs, checkboxes and switches follow native behaviour.
| Component | Group | Keys |
|---|---|---|
| Agenda / scheduler | Calendar & time | Events are focusable in time order. Alt+↑/↓ moves by the snap step, Alt+Shift+↑/↓ resizes, and Alt+←/→ changes the day. The context menu (right-click, Shift+F10) has “Change time…”, a dialog with date and time fields. This is the non-drag path for WCAG 2.5.7. |
| Gantt | Calendar & time | The treegrid is the accessible source of truth, and the timeline is aria-hidden. Use ↑/↓ for rows, ←/→ for cells (on the first cell, ← and → collapse and expand), Home and End, and Enter or F2 to edit a cell. Enter saves and Escape cancels. Date edits are the non-drag path (WCAG 2.5.7). |
| Resource calendar | Calendar & time | The grid has one tab stop. Use the arrow keys to move, Shift+arrows to select, Enter for the cell menu (assign template, remove, your actions) and Delete to clear (with Undo). Alt+arrows move the cell's blocks. In codes mode, type a code. |
| Time tracker | Calendar & time | The timesheet is a grid with one tab stop. Use the arrow keys to move, type a digit or press Enter to edit, Enter to save (then move down), Escape to cancel and Delete to clear. |
| Donut / Pie chart | Charts | The ring itself is not focusable; the legend list is the keyboard path. |
| Heatmap | Charts | Arrows move (calendar: up/down = day, left/right = week), Home / End jump within a row, PageUp / PageDown move a month (calendar) or to the column ends (matrix). |
| Comment thread | Data display | The list is an ARIA feed — PageDown / PageUp move between comments. Reply moves focus into the reply box and back after sending. |
| Filter bar | Data display | The bar is one toolbar tab stop; ←/→, Home/End move between chip segments. In the popover type to search fields, Backspace on an empty input goes back a step, Enter applies, Esc cancels. |
| Pin map | Data display | Tab enters on the selected (or first) pin, arrows go to the nearest pin in that direction, Home/End jump, Enter/Space toggles selection, Escape clears. |
| Task list | Data display | Drag reorder: wrap the list in the Sortable group when you need pointer dragging; onReorder already provides the keyboard alternative. |
| Tree view | Data display | ↑ ↓ move, → opens or enters a branch, ← closes or goes to the parent (swapped in RTL), Home / End, * opens all siblings, type-ahead (folded: “ist” finds “İstanbul”), Space toggles the check or selection, Enter runs onAction. |
| Hierarchy chart | Editors & drag-and-drop | Accessibility follows the Tree view pattern on the nodes: role="tree" with levels, positions and expanded state. ↑ / ↓ go through nodes in reading order, → opens or enters, ← closes or goes to the parent, Home / End, * opens siblings, Enter or Space selects, type-ahead by first letter. The focused node is panned into view. |
| Image cropper | Editors & drag-and-drop | Focus the crop area and use the arrow keys to move; the zoom slider takes arrows, Page Up/Down, Home/End. |
| Kanban | Editors & drag-and-drop | On a focused card, Space or Enter lifts it (arrow keys move across columns, Space drops, Esc cancels). Alt + ← / → moves to the adjacent stage (mirrored in RTL), Alt + ↑ / ↓ within the column. The card menu has Move up / down and Move to ▸ every stage. Every move is announced and focus stays on the card. |
| Permission matrix | Editors & drag-and-drop | Arrows move (roving tabindex, mirrored in RTL), Home/End go to the row edges, Ctrl+Home/End to the grid corners. Space toggles, Enter opens the scope menu, 1–4 pick a scope, Delete resets an override. The header row and row headers are part of the grid: their buttons grant or revoke a whole column or row. |
| Rich text / block editor | Editors & drag-and-drop | Esc selects the current block. In block selection, ↑/↓ move between blocks, Alt+Shift+↑/↓ moves the block, ⌘/Ctrl+Shift+D duplicates, Alt+Shift+Z deletes, Enter goes back to the text. Insertions, moves and deletes are announced. |
| Sortable | Editors & drag-and-drop | Focus a handle, Space or Enter lifts, arrow keys move, Space or Enter drops, Escape cancels. Instructions are attached to every handle via aria-describedby. |
| Sortable list / tree | Editors & drag-and-drop | Trees use the Tree view pattern: role="tree", one tab stop, ↑ / ↓ move between rows, → opens or enters, ← closes or goes to the parent, Home / End, * opens siblings, type-ahead by first letter. |
| Export | Flows & wizards | Scope and format are radio groups, columns a checkbox list with select-all and drag or keyboard reordering. File names carry the local date: contacts-2026-11-10.csv. Writing the audit entry is the app's job — onExport receives the reason. |
| Color picker | Forms & inputs | The area and strips are role=slider with dictionary names (“Saturation and brightness”, “Hue”, “Opacity”) and work with arrow keys. |
| Combobox | Forms & inputs | WAI-ARIA combobox + listbox: the search box owns aria-activedescendant; ↑/↓ move, Enter selects, Esc closes and returns focus to the trigger. |
| Line items | Forms & inputs | Enter on the last field of the last row adds a row; Alt+↑/↓ moves a row; Ctrl/⌘+Enter inserts below; Ctrl/⌘+Shift+Backspace deletes. The row menu offers the same actions without shortcuts or dragging. |
| Slider | Forms & inputs | Arrows ± step, Page Up / Page Down ± 10 steps, Home / End to the bounds. aria-valuetext carries the formatted value ("$120", "₺1.250"). |
| Tag input | Forms & inputs | Backspace on the empty input moves focus to the last chip; a second Backspace (or Delete) removes it. ←/→ move between chips and back to the input; typing on a chip returns to the input. |
| Aspect Ratio | Media | Nothing is focusable on the box itself; keyboard and screen readers reach the child directly. |
| Carousel | Media | When focus is inside: ←/→ (↑/↓ vertical), Home, End. RTL flips the arrows and the direction. |
| Image field | Media | The focal marker is a focusable 2-D control: aria-roledescription “focal point”, aria-valuetext “horizontal 62%, vertical 40%”; arrows step 1%, Shift + arrows 10%, Home centres. |
| Lightbox | Media | ←/→ browse (mirrored in RTL), Home/End, + / − / 0 zoom, Esc close. |
| Media library | Media | (grid): arrows move, Home/End (row), Ctrl/⌘ + Home/End (all), Page Up/Down, Space selects, Shift extends, Ctrl/⌘ + A selects all, Enter opens details, Delete asks to delete, Esc clears the selection. The grid uses a roving tabindex, also when virtualised. |
| Video player | Media | (focus inside the player): Space / K play, ← → 5 s, ↑ ↓ volume, M mute, C captions, F full screen. |
| Accordion | Navigation | Enter / Space toggles, ↑ ↓ move between headers, Home / End jump to the first / last. |
| Menubar | Navigation | Tab reaches the bar once; ← → move between triggers (and keep a menu open while moving); ↓, Enter or Space open; Esc closes and returns focus; typing jumps to an item. |
| Navigation Menu | Navigation | Tab between items, Enter / Space / ↓ open a panel, Tab moves into it, Escape closes and returns focus to the trigger. |
| Sidebar | Navigation | Tab moves between items (this is site navigation, not role=menu). → (← in RTL) opens a flyout and focuses its first link; ↑↓ move inside; Esc or ← closes and returns focus. |
| Context Menu | Overlays & feedback | Inside the menu works like Dropdown Menu: arrows, Home/End, typeahead, → for submenus, Escape to close. |
| Dropdown Menu | Overlays & feedback | Enter, Space or ↓ opens; ↑ ↓ Home End move; typing jumps to an item; → opens a submenu (← in RTL); Escape closes and returns focus to the trigger. |
| Hover Card | Overlays & feedback | Hover cards don't open from the keyboard or on touch. The same details must be reachable on the link target (the contact page, the author page). |
| Popover | Overlays & feedback | Escape closes the popover and returns focus to the trigger. |
| Tooltip | Overlays & feedback | The trigger gets aria-describedby pointing at the tooltip. Focus opens it, Escape closes it. |
| Resizable | Page layouts | (WAI-ARIA Window Splitter): focus the handle, then ← → (or ↑ ↓), Home / End; Enter collapses a collapsible pane. |
| Scroll Area | Page layouts | The viewport is keyboard-focusable only when its content has nothing focusable of its own. |
| Date Picker | Pickers | The field stays typeable; Alt+↓ opens the calendar, where arrows / PageUp / PageDown / Home / End move, Enter picks and Esc closes and returns focus to the field. |
| Date range picker | Pickers | Presets are a listbox (↑ ↓ Home End, Enter). The grid uses the WAI-ARIA grid keyboard model. The selected range is announced: “From November 1, 2026 to November 10, 2026, 10 days”. |
Known issues
- Status colours as plain text. The light-theme success colour was darkened from
#0f8a5f(4.36:1 on white) to#0b7d55(5.15:1), and badges and alerts mix the status hue with the text colour, so their 11–13 px labels pass 4.5:1. The raw hues are still below 4.5:1 as small text directly on the light page background (--scene): success 4.48:1, danger 4.39:1, warning 3.41:1. Use them for icons, dots and large text, or usetone-soft/ Badge / Alert for small text. - Hydration id mismatch under load. A
useIdmismatch was seen twice on the development server under memory pressure (maintenance and list blocks). It has not reproduced in the static build that ui.vamos.ist serves. While it happens, anaria-labelledbycan point at a stale id until React re-renders. - Unlabelled tables in two blocks (a detail block and an app shell) log a console warning; they need a caption or
aria-label. - Floating contact dock can overlap the booking bar on the restaurant and spa example sites at phone width.
- Charts are images to assistive technology. Each chart frame has a table view and a text summary; the plot itself is not navigable.
- Hover cards do not open from the keyboard or on touch, by design. Their content must be reachable on the link target.
- No manual screen-reader pass (VoiceOver, NVDA, JAWS) has been recorded for every component yet. That is the next step before 1.0.
Your part
- Give every icon-only button a
labeloraria-label, and every field a visible label throughField. - Keep one
h1per page and a<main>landmark; the app shell and site shell provide the others. - A custom accent is contrast-checked by
npx vamos-ui brand check; runnpx vamos-ui verifyin CI for the lint rules. - Right-click and drag can never be the only path: pair context menus with an action menu and drag with keyboard moves (the kit's sortable parts do this).
Reporting a problem
Found a barrier? Open an issue on GitHub with the component, theme axes, browser and assistive technology. Accessibility bugs are treated like functional bugs.