Accessibility
How Lumeo approaches accessibility, and an honest, data-driven look at what is actually tested today — per component, generated from the real test suite.
Our approach
Accessibility is not an afterthought in Lumeo. Every component is designed from the start with proper semantic markup, ARIA attributes, keyboard interactions, and focus management. Our goal is to make it easy to build inclusive applications without requiring accessibility expertise from every developer on your team.
Lumeo components follow the WAI-ARIA Authoring Practices Guide for interaction patterns, and use native HTML semantics wherever possible. When ARIA roles are necessary, they are applied automatically so you do not need to add them manually.
Accessibility is continuous work, not a checkbox: the automated audits below run on an ongoing basis, and what they find flows straight back into the library as fixes — most recently keyboard navigation for the FileManager folder tree and popup semantics on the Cascader trigger, both found by the screen-reader audit described below.
Keyboard navigation
All interactive components in Lumeo are fully operable via keyboard. The library follows standard keyboard conventions so users who rely on keyboard navigation will find familiar interaction patterns.
General patterns
- Tab moves focus between interactive elements. Shift+Tab moves focus backward.
- Enter or Space activates buttons, toggles, checkboxes, and selectable items.
- Escape closes overlays including Dialog, Sheet, Drawer, Alert Dialog, Dropdown Menu, Popover, Context Menu, Combobox, and Color Picker.
- Arrow keys navigate within composite widgets like Select, Combobox, Dropdown Menu, Context Menu, Menubar, Tabs, Rating, Carousel, and OTP Input.
- Home and End jump to the first and last item in lists and menus.
Component-specific keyboard support
| Component | Keys | Behavior |
|---|---|---|
| Select | Up Down Enter Escape | Navigate options, select, close |
| Combobox | Up Down Enter Escape | Navigate filtered results, select, close |
| Dropdown Menu | Up Down Enter Escape | Navigate items, activate, close |
| Context Menu | Up Down Enter Escape | Navigate items, activate, close |
| Tabs | Left Right (or Up Down vertical) | Switch between tabs based on orientation |
| Dialog / Sheet / Drawer | Escape Tab | Close overlay, cycle focus within trap |
| Alert Dialog | Escape Tab | Close (if allowed), cycle focus within trap |
| Accordion | Enter Space | Toggle section open/closed |
| Carousel | Left Right | Navigate between slides |
| Rating | Left Right | Decrease or increase rating value |
| OTP Input | Left Right Backspace | Move between digits, delete and move back |
| Cascader | Up Down Right Escape | Navigate levels, expand children, close |
Focus management
Proper focus management is critical for keyboard and screen reader users. Lumeo handles focus automatically in overlay components so you do not need to manage it manually.
Focus trapping
When a modal overlay opens (Dialog, Sheet, Drawer, Alert Dialog), Lumeo automatically traps focus
inside the overlay content. Pressing Tab cycles through focusable elements within the overlay,
and focus cannot escape to the page behind it. This is implemented via
ComponentInteropService.SetupFocusTrap()
and cleaned up with
RemoveFocusTrap()
when the overlay closes.
Scroll locking
When a modal overlay is open, background scroll is locked via
ComponentInteropService.LockScroll()
to prevent users from accidentally scrolling the page behind the overlay.
Scroll is restored when the overlay closes. This applies to Dialog, Sheet, Drawer, Alert Dialog, Tour, and Image preview.
Click-outside dismissal
Non-modal overlays like Popover, Dropdown Menu, Context Menu, Combobox, and Color Picker register
click-outside handlers via
ComponentInteropService.RegisterClickOutside().
Clicking outside the overlay content dismisses it, which is the expected behavior for these patterns.
Focus-visible rings
All interactive elements display a visible focus ring when navigated to via keyboard. The ring uses the
focus-visible
pseudo-class, so it only appears for keyboard users and does not show on mouse click. The ring color
is set via the --ring
CSS variable, which adapts to light and dark mode.
Cleanup
All overlay components implement IAsyncDisposable
to properly clean up focus traps, scroll locks, and click-outside handlers. Cleanup code handles
JSDisconnectedException
gracefully to avoid errors during Blazor circuit disconnection.
Screen reader support
Lumeo components use semantic HTML elements and ARIA attributes to provide a meaningful experience for screen reader users. Roles, states, and properties are applied automatically.
ARIA roles and attributes by component
| Component | ARIA Role | Key Attributes |
|---|---|---|
| Dialog | dialog |
aria-modal, aria-labelledby, aria-describedby |
| Alert Dialog | alertdialog |
aria-modal, aria-labelledby, aria-describedby |
| Sheet / Drawer | dialog |
aria-modal, aria-labelledby, aria-describedby |
| Alert | alert |
Dismiss button has aria-label |
| Toast | alert |
Live region announcements for screen readers |
| Checkbox | checkbox |
aria-checked (supports mixed for indeterminate) |
| Switch | switch |
aria-checked |
| Toggle | button | aria-pressed |
| Toggle Group | group |
Each item has aria-pressed |
| Radio Group | radiogroup |
aria-label, aria-labelledby; items have aria-checked |
| Select | combobox / listbox |
aria-haspopup, aria-expanded, aria-controls, aria-activedescendant |
| Combobox | combobox / listbox |
aria-haspopup, aria-expanded, aria-autocomplete, aria-activedescendant |
| Dropdown Menu | button / menu | aria-haspopup, aria-expanded, aria-controls; items have menuitem |
| Context Menu | menuitem / menuitemcheckbox / menuitemradio |
aria-checked on checkbox and radio items |
| Tabs | tablist / tab / tabpanel |
aria-selected, aria-controls, aria-orientation, aria-labelledby |
| Accordion | button | aria-expanded on trigger buttons |
| Rating | radiogroup |
aria-label ("Rate X out of Y") on each star |
| Progress | progressbar |
aria-valuenow, aria-valuemin, aria-valuemax |
| Breadcrumb | navigation |
aria-label="breadcrumb"; current page has aria-current="page" |
| Pagination | navigation |
aria-label="pagination" |
| Carousel | region |
aria-roledescription="carousel"; slides have aria-roledescription="slide" |
| Tooltip | tooltip |
Content announced on focus/hover |
| Tree View / Tree Select | tree / treeitem |
aria-expanded, aria-selected |
| Menubar | menubar |
Items use menuitem role |
| Steps | list / listitem |
Semantic step progression |
Color and contrast
Lumeo uses CSS custom properties for all colors, which means color contrast can be controlled at the theme level rather than per component.
-
No hardcoded colors. Every color in Lumeo is a CSS variable (
--foreground,--primary,--muted, etc.). Swapping the theme swaps all colors at once. -
Dark mode via variable swaps. Dark mode is handled entirely through CSS variable overrides. There are no
dark:Tailwind prefixes anywhere in the library, ensuring a consistent and clean approach. -
Foreground / background pairing. Every background color variable has a matching foreground variable (e.g.
--primarypaired with--primary-foreground) to maintain readable contrast ratios. - Built-in themes tested for contrast. The 7 built-in color themes (Zinc, Blue, Green, Rose, Violet, Amber, Teal) are designed with sufficient contrast between foreground and background in both light and dark modes.
-
Muted text uses proper contrast. The
--muted-foregroundvariable is calibrated to provide readable secondary text without disappearing against the background.
Per-component test matrix
This is a status report, not a marketing claim. The table below is generated at runtime from
registry.json,
which Lumeo.RegistryGen produces directly from the real test sources — so it cannot drift from what is
actually tested. It reflects the state of the suite, not aspiration.
What the automated tests actually assert
-
A11y assertions (bUnit). Render-level checks that
the correct ARIA role, state attributes (
aria-*), and focus behavior are present on the rendered markup for a given component. - Keyboard tests (bUnit). Simulated key events (Tab, Arrow keys, Enter, Escape, etc. as applicable) verified to trigger the expected interaction — focus moves, selection changes, overlays close.
-
E2E coverage (Playwright). Real-browser end-to-end
tests that exercise a full user flow through the component, for the subset of components
where
testCoverage.e2eis true. -
Screen-reader audit (NVDA, automated).
scripts/sr-auditdrives a real NVDA instance (via Guidepup) against every component docs route and captures what it actually speaks. The 18 highest-keyboard-surface components are verified against the expected announcements of the screen-reader protocol (27/27 checks passing, run 2026-07-13); every other page is probed for a genuine focus announcement of its demo element — 134 of the remaining 146 announce correctly; the other 12 have no keyboard-focusable demo element (mostly display-only components, plus a few link-based or pointer-driven demos tracked for follow-up). Findings become fixes: this audit is what surfaced the FileManager tree keyboard gap and the Cascader trigger's missing popup semantics.
What has not been done yet
- No third-party accessibility audit has been commissioned.
- The axe-core sweep runs weekly (and on-demand) in CI against all 164 component routes, not per-commit or per-PR — a full sweep is a build + boot + 164-page crawl, too slow for the PR critical path. See the results below.
- No VoiceOver (macOS) pass yet — the automated screen-reader audit above currently covers NVDA on Windows only. Screen-reader feedback is genuinely welcomed — please open a GitHub issue if something doesn't announce correctly.
134/165 components have automated a11y assertions · 104/165 have keyboard interaction tests · 17/165 have E2E coverage.
| Component | A11y assertions | Keyboard tests | E2E coverage | Status |
|---|---|---|---|---|
| Accordion | ✓ | ✓ | — | Tested (automated) |
| Affix | — | — | — | Not yet audited |
| AgentMessageList | ✓ | ✓ | — | Tested (automated) |
| Alert | ✓ | ✓ | — | Tested (automated) |
| AlertDialog | ✓ | ✓ | — | Tested (automated) |
| AnimatedBeam | — | — | — | Not yet audited |
| AppBar | — | — | — | Not yet audited |
| AspectRatio | — | — | — | Not yet audited |
| AudioPlayer | ✓ | ✓ | — | Tested (automated) |
| Avatar | ✓ | — | — | Partially tested |
| BackToTop | ✓ | ✓ | — | Tested (automated) |
| Badge | ✓ | — | ✓ | Partially tested |
| Barcode | ✓ | — | — | Partially tested |
| Bento | — | — | — | Not yet audited |
| BlurFade | — | — | — | Not yet audited |
| BorderBeam | ✓ | — | — | Partially tested |
| BottomNav | ✓ | ✓ | — | Tested (automated) |
| Breadcrumb | ✓ | ✓ | — | Tested (automated) |
| Button | ✓ | ✓ | — | Tested (automated) |
| ButtonGroup | ✓ | ✓ | — | Tested (automated) |
| CTASection | ✓ | — | — | Partially tested |
| Calendar | ✓ | ✓ | — | Tested (automated) |
| Card | ✓ | ✓ | — | Tested (automated) |
| Carousel | ✓ | ✓ | — | Tested (automated) |
| Cascader | ✓ | ✓ | — | Tested (automated) |
| Center | — | — | — | Not yet audited |
| Chart | ✓ | ✓ | — | Tested (automated) |
| Checkbox | ✓ | ✓ | — | Tested (automated) |
| Chip | ✓ | ✓ | — | Tested (automated) |
| Code | — | — | — | Not yet audited |
| CodeEditor | ✓ | — | — | Partially tested |
| Collapsible | ✓ | ✓ | — | Tested (automated) |
| ColorPicker | ✓ | ✓ | — | Tested (automated) |
| Combobox | ✓ | ✓ | — | Tested (automated) |
| Command | ✓ | ✓ | — | Tested (automated) |
| Confetti | — | — | — | Not yet audited |
| ConfirmButton | — | ✓ | — | Partially tested |
| ConsentBanner | ✓ | ✓ | — | Tested (automated) |
| Container | — | — | — | Not yet audited |
| ContextMenu | ✓ | ✓ | — | Tested (automated) |
| DataGrid | ✓ | ✓ | ✓ | Tested (automated) |
| DataTable | ✓ | ✓ | — | Tested (automated) |
| DatePicker | ✓ | ✓ | — | Tested (automated) |
| DateTimePicker | ✓ | ✓ | — | Tested (automated) |
| Delta | — | — | — | Not yet audited |
| DensityScope | ✓ | — | — | Partially tested |
| Descriptions | — | — | — | Not yet audited |
| Dialog | ✓ | ✓ | ✓ | Tested (automated) |
| DirectionProvider | ✓ | ✓ | — | Tested (automated) |
| Dock | ✓ | ✓ | — | Tested (automated) |
| Drawer | ✓ | ✓ | ✓ | Tested (automated) |
| DropdownButton | ✓ | ✓ | — | Tested (automated) |
| DropdownMenu | ✓ | ✓ | ✓ | Tested (automated) |
| EmptyState | ✓ | — | — | Partially tested |
| FeatureGrid | ✓ | — | — | Partially tested |
| FeatureItem | — | — | — | Not yet audited |
| FileManager | ✓ | ✓ | — | Tested (automated) |
| FileUpload | ✓ | ✓ | — | Tested (automated) |
| FileViewer | ✓ | ✓ | — | Tested (automated) |
| Filter | ✓ | ✓ | — | Tested (automated) |
| Flex | — | — | — | Not yet audited |
| Form | ✓ | — | — | Partially tested |
| Gantt | ✓ | ✓ | ✓ | Tested (automated) |
| GanttChart | ✓ | ✓ | ✓ | Tested (automated) |
| Gauge | ✓ | — | — | Partially tested |
| Grid | — | — | — | Not yet audited |
| Heading | ✓ | — | — | Partially tested |
| Hero | ✓ | — | — | Partially tested |
| Highlighter | — | — | — | Not yet audited |
| HoverCard | ✓ | ✓ | — | Tested (automated) |
| Icon | ✓ | — | — | Partially tested |
| Image | ✓ | ✓ | — | Tested (automated) |
| ImageCompare | ✓ | ✓ | — | Tested (automated) |
| InplaceEditor | ✓ | ✓ | — | Tested (automated) |
| Input | ✓ | ✓ | — | Tested (automated) |
| InputMask | — | ✓ | — | Partially tested |
| Kanban | ✓ | ✓ | — | Tested (automated) |
| Kbd | ✓ | ✓ | — | Tested (automated) |
| KpiCard | — | — | — | Not yet audited |
| Label | ✓ | — | — | Partially tested |
| Link | ✓ | ✓ | — | Tested (automated) |
| List | ✓ | ✓ | — | Tested (automated) |
| Map | ✓ | — | ✓ | Partially tested |
| Marquee | ✓ | — | — | Partially tested |
| MegaMenu | ✓ | ✓ | — | Tested (automated) |
| Mention | ✓ | ✓ | — | Tested (automated) |
| Menubar | ✓ | ✓ | ✓ | Tested (automated) |
| NavigationMenu | ✓ | ✓ | — | Tested (automated) |
| NumberInput | ✓ | ✓ | — | Tested (automated) |
| NumberTicker | — | — | — | Not yet audited |
| OtpInput | ✓ | ✓ | — | Tested (automated) |
| Overlay | ✓ | — | — | Partially tested |
| OverlayForm | ✓ | — | — | Partially tested |
| Pagination | ✓ | ✓ | — | Tested (automated) |
| PasswordInput | ✓ | ✓ | — | Tested (automated) |
| PdfViewer | ✓ | ✓ | — | Tested (automated) |
| PickList | ✓ | ✓ | — | Tested (automated) |
| PivotGrid | ✓ | ✓ | — | Tested (automated) |
| PopConfirm | ✓ | ✓ | — | Tested (automated) |
| Popover | ✓ | ✓ | ✓ | Tested (automated) |
| Progress | ✓ | — | — | Partially tested |
| PromptInput | ✓ | ✓ | — | Tested (automated) |
| PullToRefresh | ✓ | — | — | Partially tested |
| QRCode | ✓ | — | — | Partially tested |
| QueryBuilder | ✓ | ✓ | — | Tested (automated) |
| RadioGroup | ✓ | ✓ | — | Tested (automated) |
| Rating | ✓ | ✓ | — | Tested (automated) |
| ReasoningDisplay | ✓ | — | — | Partially tested |
| Resizable | ✓ | ✓ | — | Tested (automated) |
| Result | ✓ | — | — | Partially tested |
| RichTextEditor | ✓ | ✓ | — | Tested (automated) |
| RingProgress | ✓ | — | — | Partially tested |
| SafeArea | — | — | — | Not yet audited |
| Scheduler | ✓ | ✓ | ✓ | Tested (automated) |
| ScrollArea | ✓ | ✓ | — | Tested (automated) |
| Scrollspy | ✓ | ✓ | — | Tested (automated) |
| Segmented | ✓ | ✓ | — | Tested (automated) |
| Select | ✓ | ✓ | ✓ | Tested (automated) |
| Separator | ✓ | — | — | Partially tested |
| Sheet | ✓ | ✓ | ✓ | Tested (automated) |
| ShimmerButton | — | — | — | Not yet audited |
| Sidebar | ✓ | ✓ | — | Tested (automated) |
| SignaturePad | ✓ | ✓ | — | Tested (automated) |
| Skeleton | ✓ | — | — | Partially tested |
| Slider | ✓ | ✓ | ✓ | Tested (automated) |
| Sortable | ✓ | ✓ | ✓ | Tested (automated) |
| Spacer | — | — | — | Not yet audited |
| SparkCard | ✓ | — | — | Partially tested |
| Sparkles | — | — | — | Not yet audited |
| Sparkline | ✓ | — | — | Partially tested |
| SpeedDial | ✓ | ✓ | — | Tested (automated) |
| Spinner | ✓ | — | — | Partially tested |
| SplitButton | ✓ | ✓ | — | Tested (automated) |
| Splitter | ✓ | ✓ | ✓ | Tested (automated) |
| Stack | — | — | — | Not yet audited |
| Statistic | ✓ | — | — | Partially tested |
| Stepper | ✓ | ✓ | — | Tested (automated) |
| Steps | ✓ | ✓ | — | Tested (automated) |
| StreamingText | — | — | — | Not yet audited |
| SwipeActions | ✓ | ✓ | — | Tested (automated) |
| Switch | ✓ | ✓ | — | Tested (automated) |
| Table | ✓ | — | — | Partially tested |
| Tabs | ✓ | ✓ | — | Tested (automated) |
| TagInput | ✓ | ✓ | — | Tested (automated) |
| Text | — | — | — | Not yet audited |
| TextReveal | — | — | — | Not yet audited |
| Textarea | ✓ | ✓ | — | Tested (automated) |
| ThemeSwitcher | ✓ | ✓ | — | Tested (automated) |
| ThemeToggle | ✓ | ✓ | — | Tested (automated) |
| TimePicker | ✓ | ✓ | — | Tested (automated) |
| Timeline | ✓ | — | — | Partially tested |
| Toast | ✓ | ✓ | — | Tested (automated) |
| Toggle | ✓ | ✓ | — | Tested (automated) |
| ToggleGroup | ✓ | ✓ | — | Tested (automated) |
| ToolCallCard | ✓ | ✓ | — | Tested (automated) |
| Toolbar | ✓ | ✓ | — | Tested (automated) |
| Tooltip | ✓ | ✓ | ✓ | Tested (automated) |
| TouchRipple | — | — | — | Not yet audited |
| Tour | ✓ | ✓ | — | Tested (automated) |
| Transfer | ✓ | ✓ | — | Tested (automated) |
| TreeSelect | ✓ | ✓ | — | Tested (automated) |
| TreeView | ✓ | ✓ | — | Tested (automated) |
| UploadTrigger | — | ✓ | — | Partially tested |
| Watermark | — | — | — | Not yet audited |
| Window | ✓ | ✓ | — | Tested (automated) |
Axe-core audit results
scripts/a11y-audit
runs an axe-core WCAG A/AA sweep of every /components/<slug>
docs route (all 164), scoped to each page's <main>
content so shared app-shell chrome doesn't spam every report with the same findings. It runs weekly
and on-demand in CI, gated against a committed baseline of accepted-but-not-yet-fixed findings —
a pull request fails the gate only if it introduces a new (component, rule) violation, so
the backlog can only shrink, never silently grow.
138 accepted critical/serious findings (last triage 2026-07-12)
These are tracked, known-not-yet-fixed violations across the component set — not zero. Each fix removes its entry from the baseline in the same PR; any new (component, rule) pair not already in the baseline fails CI. Confirmed false positives (shared docs-chrome or harness artifacts, e.g. third-party embedded content like the PDF.js canvas or MapLibre GL map) are excluded entirely viaexclusions.json
and aren't counted here.
Last triage: 2026-07-12. Tool: axe-core 4.12.1 · rules: wcag2a, wcag2aa, best-practice.
See scripts/a11y-audit/README.md
for how to run the sweep locally and how the baseline/exclusions gate works.
Best practices for your app
While Lumeo handles component-level accessibility, there are application-level practices you should follow to ensure a fully accessible experience.
-
Use Label with form inputs. Always pair Input, Select, Checkbox, Radio Group, Switch, and other form controls with a
<Label>component. This creates an accessible name for screen readers. -
Provide meaningful text. Avoid icon-only buttons without an
aria-label. If a button only has an icon, passaria-label="Description"via AdditionalAttributes. -
Use semantic headings. Use the
<Heading>component with appropriateLevelvalues (1 through 6) to create a logical heading hierarchy. Screen readers use headings to navigate page structure. - Test with keyboard only. Try navigating your application using only the keyboard. Every action should be reachable without a mouse.
- Test with a screen reader. Run through key flows with NVDA (Windows), VoiceOver (macOS), or Narrator (Windows) to verify the experience.
-
Respect user preferences. Lumeo supports
prefers-color-schemefor automatic dark mode. Avoid overriding user system preferences without offering a toggle.