Accessibility (A11y) Decision Matrix: When to Use What
This guide establishes the architectural and accessibility standards for mutually exclusive switchers, tabs, action groups, and navigation controls across Waldur HomePort. It provides explicit decision trees and code recipes for developers and AI coding agents (LLMs).
1. Quick Decision Rules
Follow this decision order to determine the correct component:
- Does it change the URL or navigate to another page/state?
➔ Navigation Link (@/core/Linkor<nav>+<a>). - Does it trigger an immediate action, preset, or calculation?
➔ Action Button Group (BaseButtonwithrole="group"and optionalaria-pressed). - Can multiple options be active simultaneously?
➔ Multi-select Buttons / Checkboxes (role="group"witharia-pressedorCheckbox). - Does each option own a distinct, large content panel?
➔ Tabs Pattern (RadixTabsstyled viasegmentedListClassName/segmentedItemClassName). - Is it a question in a submission form with validation errors?
➔ Form RadioGroup (RadioGroupinside<fieldset>+<legend>). - Does it switch a display lens, interval, or in-memory list filter?
➔ View Lens / Filter (SegmentedControlwithrole="radiogroup").
2. Comparative Matrix
| UI Pattern | Standard Component | Underlying Semantics | Keyboard Model | Valid Contexts | Prohibited Anti-Patterns |
|---|---|---|---|---|---|
| View Lens / Filter | SegmentedControl (waldur-ui) |
role="radiogroup"role="radio"aria-checked |
Single Tab stop; ← / → select option |
Filtering a list in place (All/Unread), chart intervals (Day/Month), diff modes (Table/JSON) | Never use for actions (+1h extend), routing tabs, or tab panels |
| Action Preset / Shortcut | Group of BaseButton (waldur-ui) |
role="group"<button>aria-pressed |
Normal Tab stop per button; Enter / Space activates |
Offset buttons (+30m, +1h), preset calculators, filter clearers | Never use SegmentedControl (radios imply persistent state, not actions) |
| Tabbed Panels | Radix Tabs (or Tab.Container) |
role="tablist"role="tab"role="tabpanel"aria-controls |
Single Tab stop; ← / → moves tabs; Space / Enter activates |
Swapping major DOM sections (e.g. Overview vs Settings vs Audit Log) | Never use SegmentedControl without role="tabpanel" and aria-controls |
| Page Navigation | @/core/Link or <nav> + <a> |
<nav aria-label="..."><a href="..."> |
Normal Tab per link; Enter navigates; Right-click "Open in new tab" |
UI-Router state tabs (TableTabs), page headers, sidebar links |
Never use SegmentedControl or <button> for URL transitions |
| Form Question | RadioGroup (waldur-ui) |
<fieldset><legend>native <input type="radio"> |
Single Tab stop; Arrow keys select |
Standard form questions with error messages and form serialization | Don't use SegmentedControl when validation errors or descriptions are needed |
| Multi-Select Filter | Button group with aria-pressed |
role="group"<button aria-pressed="..."> |
Tab between buttons; Space toggles |
Filter tags, multi-select chips, facet bars | SegmentedControl strictly supports one active value |
3. Pattern Implementation Guides
Pattern 1: View Lens / Display Filter (SegmentedControl)
Use when changing how current data is viewed without navigating away or unmounting surrounding layout.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | |
A11y Rules for SegmentedControl:
- Mandatory accessible name: Must supply
aria-labeloraria-labelledby. Screen readers announce:"{aria-label}, radio group. Table, radio button, checked, 1 of 2". - Value must never be empty: If state can be cleared, use toggle buttons, not a radio group.
- No action side-effects: Selecting an option should only update the view lens/filter parameter.
Pattern 2: Action Presets & Shortcuts (BaseButton Group)
Use when buttons trigger an immediate operation or calculation (e.g. calculating dates, triggering presets).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | |
Why SegmentedControl is an Anti-Pattern here:
- In Radix
RadioGroup,onValueChangeonly triggers when changing to a different value. If a user clicks+30 min, manually adjusts the input, and clicks+30 minagain,RadioGroupdrops the interaction. - Screen readers announce
"Radio button, checked", misleading users to believe they are selecting a form field setting rather than executing an action.
Pattern 3: Segmented Tab Panels (Radix Tabs with Segmented Styling)
When you want the sleek segmented look (inset shadows, connected borders) for full tab panels:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | |
Why this is superior to SegmentedControl:
- Emits WAI-ARIA
role="tablist",role="tab", androle="tabpanel". - Establishes
aria-controlslinks from triggers to content containers. - Assistive technologies announce
"Tab 1 of 2"and support shortcuts to navigate into the active panel.
Pattern 4: Page & Route Navigation (TableTabs / @/core/Link)
When selecting a tab changes the URL or router state (router.stateService.go):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Prohibited Anti-Pattern:
- Never replace routing links with
SegmentedControlor plain<button>elements. Doing so breaks middle-click, "Copy link address", search-engine indexing, and screen reader link announcements.
4. Checklist for Developers & LLMs
Before choosing or migrating a switcher component, run this checklist:
- What happens on click?
- Updates URL/route? ➔ Use
Link/<nav>. - Triggers calculation or modal action? ➔ Use
BaseButton(role="group"+aria-pressed). - Swaps a large tab panel? ➔ Use Radix
TabswithsegmentedItemClassName. - Filters or changes view parameters in place? ➔ Use
SegmentedControl. - Does
SegmentedControlhave an accessible label? - Every
<SegmentedControl>must havearia-label={translate('...')}oraria-labelledby. - Are disabled buttons compliant?
- Any disabled
BaseButtonmust supplydisabledReasonortooltipper thewaldur-custom/enforce-disabled-button-tooltiplint rule. - Is single-selection strictly guaranteed?
- If the user can deselect the option, do not use
SegmentedControl.