Test solutions
Interactive demos only — try both patterns side by side. Open the other tabs for explanation, static states, and handoff notes.
A · Dual-layer (type in trigger)
Focus keeps the avatar and shows a caret in the name. Full chrome hides when you type to filter. Keys: ↓ ↑ Enter Esc
Choose a person. Type to search.
B · Nested search (start icon)
Open moves focus into search. Selected value shows start icon + label. Keys: Enter/↓ open · ↑/↓ · Esc
Where should we send updates?
1. The problem with the original dropdown
Before the 2026-06-24 accessibility fix, searchable single-select showed a rich selected value
(icon / avatar / custom renderer) as a visual overlay on top of an
empty <input role="combobox">.
What users saw
Looks selected. Avatar + name are painted over the field.
What assistive tech heard
WCAG 4.1.2 Name, Role, Value — value was not programmatically determinable.
value.
What changed in the fix (and the new trade-off)
| Aspect | Before (overlay) | After (value in input) |
|---|---|---|
| Input value after select | Empty | Selected label |
| How selection is shown | Visual overlay over empty field | Inside the input |
| Screen reader on focus | “… blank” | “… Julia Martinez” |
| Collapsed selected value | Rich (icon / avatar / valueRenderer) | Text-only |
startElement, avatars,
end elements, and searchable valueRenderer. Dual-layer restores the rich
collapsed look without emptying the input.
2. Solution: dual-layer selected value
Keep engineering’s rule — the selected label lives in the native input — and add a decorative rich layer for sighted users. Never go back to an empty input.
input.value = "Julia Martinez"Screen readers announce the real value. Meets WCAG 4.1.2.
aria-hidden="true",
pointer-events none). Input text is visually transparent while selected — not removed.
Static stages
1 · Default (empty)
PersonPlaceholder only. Visible label remains.
2 · Collapsed selected (rich)
PersonAvatar visible. value still = “Julia Martinez”.
3 · Filtering (rich hidden)
Person- JM Julia Martinez ✓
- JL Jordan Lee
User is editing a query — show real input text, hide value chrome.
4 · Reopen with selection
Person- JL Jordan Lee
- JM Julia Martinez ✓
- SO Sam Okonkwo
Full list on reopen (per eng behavior). Selected option marked visually + aria-selected.
3. Why dual-layer works vs the documented problems
Mapped directly to the issues called out in the component docs / change notes.
| Problem from screenshots / docs | Why dual-layer addresses it |
|---|---|
| SR announced “blank” when selection was only an overlay on an empty input (4.1.2). | Input is never emptied to show chrome. value stays the label. Overlay is decorative only. |
| Native input can only hold a string — so icons were dropped after the fix. | True for the value. Icons live in a separate visual layer; they don’t need to be inside the string. |
| Collapsed lost startElement / avatar / valueRenderer for searchable selects. | Collapsed can look rich again. Renderer = visual mirror synced to the same label string AT reads. |
helperText must stay tied via aria-describedby (1.3.1). |
Unchanged — dual-layer doesn’t replace labeling or description wiring. |
| Focus must remain on the input after select. | Same editable combobox model; focus stays on the field; rich layer has pointer-events: none. |
| Type to filter / clear text restores full list. | On type, hide rich layer and show filter string. Behavior matches eng’s interaction table. |
Dual-layer (proposed): rich overlay + filled input → icons for sighted users, value for AT.
What this does not claim
- Complex interactive content inside the collapsed value (links, nested buttons) — still out of scope; keep those in the list.
- End suffixes / heavy custom layouts may still be list-only if they can’t sync cleanly to a single label string.
- Implementation needs QA: high contrast, zoom, VoiceOver/NVDA, mobile caret behavior.
4. Nested-search pattern (with start icon) — accessible version
Matches the design frame: trigger field + popup with its own search row (magnifying-glass start icon) and option list. Below is the same layout made accessible — not the broken “focus stays on the outer field while typing happens in a second input” version.
Static states (aligned to the design)
Default
ChannelWhere should we send updates?
Hover
ChannelWhere should we send updates?
Active (search focused)
Channel- In-app
- Slack
- Mobile push
- SMS
Focus is in the search field (blue ring), not left on the outer trigger.
Selected (start icon)
ChannelWhere should we send updates?
Accessibility changes applied
- Pattern: collapsed control is a
button(can host start icon in real DOM). Search lives only in the popup. - Focus on open: moves to the filter input — not left on the outer field while a second input is the typing surface.
- Names: visible label “Channel”; search
aria-label="Filter channels"; cleararia-label="Clear selection"; start iconsaria-hidden. - Selected value: trigger accessible name includes the label text (“Channel, Email”) so AT is never blank — icons stay decorative.
- List semantics: options are
role="option"; search row is outside the listbox options. - Placeholder ≠ label: “Select a channel” / “Search for channel” are hints only.
5. Handoff checklist
- When selected and not filtering: set
input.valueto option label; show decorativestartElement/ avatar witharia-hidden="true". - Visually hide input text (e.g. transparent) — do not clear
value. - On focus with selection: keep avatar; show real input text + caret (hide overlay label). On type / filter: hide full rich layer.
- On select / blur with selection (and not filtering): restore rich layer; keep focus on input.
- Keep existing props:
label,helperText+aria-describedby,clearAriaLabel,noOptionsMessage. - Prefer
aria-haspopup="listbox"when the popup is a listbox (align with APG); confirm against current dialog markup. - Document for Storybook: “Searchable collapsed value may look rich; accessible value is always the label string.”
- If using nested search (section 4): on open move focus to filter input; trigger name includes selected label; start icons
aria-hidden; search not an option.