Custom Dropdown Menus: The WAI-ARIA Select Pattern for E-Commerce Sort, Currency, and Country Selectors
Quietramp ·
Custom Dropdown Menus: The WAI-ARIA Select Pattern for E-Commerce Sort, Currency, and Country Selectors
A “Sort by: Price, low to high” menu, a currency switcher in the header, a country selector on a shipping form — almost every e-commerce site has at least one single-choice dropdown that isn’t a native <select> element. It’s usually replaced because the browser’s default <select> styling can’t be customized enough to match a design system: no control over the popup’s font, spacing, or animation. The replacement is a <div>-based trigger and a styled list of options — and unlike a native <select>, none of the keyboard support, focus handling, or screen-reader announcement comes for free anymore.
Quick answer: a custom, non-native dropdown needs the trigger marked up as role="combobox" with aria-expanded and aria-controls, the popup marked up as role="listbox" with role="option" children and aria-selected on the current choice, aria-activedescendant tracking the highlighted option without moving real keyboard focus off the trigger, and full keyboard support — Enter/Space/arrow keys to open and navigate, typing a letter to jump to a matching option, Escape to close without changing the value (WCAG 4.1.2, 2.1.1, 1.3.1). This is the WAI-ARIA Authoring Practices Guide’s own “select-only combobox” pattern — a documented reference implementation, not something to improvise per project.
This article covers technical accessibility practice, not legal advice — it explains what WCAG 2.1 AA requires for this component and how to meet it, not whether a specific implementation carries legal risk.
First: does this need to be custom at all?
Before reaching for ARIA, check whether a native <select> restyled with CSS — not replaced with a <div> — actually meets the design. A native <select> gets keyboard support, screen-reader announcement, and mobile-native picker behavior for free, and appearance: none plus custom borders, backgrounds, and an icon covers a lot of visual customization without touching the underlying element. If the real blocker is the popup’s own appearance, which the browser still renders natively even with appearance: none on the closed control, a custom build is the right call. If CSS could actually solve it, a native <select> is less code and fewer ways to get this wrong.
What “select-only” means, and why it’s a different pattern than search autocomplete
Not every dropdown is the same widget. The WAI-ARIA APG’s Combobox pattern overview draws the line directly: “if a combobox does not support text input, it is referred to as select-only, meaning the only way users can set its value is by selecting a value in the popup.” That’s a sort-by menu, a currency switcher, a country selector — the user picks from a fixed list, not typed filtering. An editable combobox is a text field where typing filters the options shown instead — the pattern behind a search-suggestions box, which our article on accessible search autocomplete already covers. The two share some ARIA vocabulary (role="combobox", a popup role="listbox"), but they’re built for different interactions — a select-only dropdown doesn’t need an <input> element at all.
The pattern: trigger, popup, and how they connect
The WAI-ARIA APG’s select-only combobox example is the reference implementation for this component. It’s explicit that the trigger is “not made with an <input> element, and it does not accept freeform user input,” while still letting “users type characters to select matching options” the same way a native <select> supports type-ahead.
The trigger carries role="combobox", an accessible name (via a visible label, connected through aria-labelledby), aria-expanded ("false" closed, "true" open), and aria-controls pointing at the popup’s id — the same WCAG 1.3.1 Info and Relationships (Level A) requirement that governs any custom widget with a visually-obvious-but-not-programmatically-obvious connection between two elements: “information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.”
The popup carries role="listbox", with each choice marked role="option" and the currently selected one carrying aria-selected="true".
Keeping the highlighted option visible to a screen reader without moving real focus is the part most hand-rolled versions skip. DOM focus stays on the trigger the entire time the popup is open; aria-activedescendant on the trigger is updated to reference whichever option is currently highlighted, and the browser announces it as if focus had landed there. This is the same mechanism our search-autocomplete article documents for the editable variant — a codebase that already implements one correctly can reuse the technique for the other.
<span id="sort-label">Sort by</span>
<div
id="sort-trigger"
role="combobox"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="sort-listbox"
aria-labelledby="sort-label sort-trigger"
aria-activedescendant=""
tabindex="0"
>
Featured
</div>
<ul id="sort-listbox" role="listbox" aria-labelledby="sort-label" hidden>
<li id="opt-featured" role="option" aria-selected="true">Featured</li>
<li id="opt-price-asc" role="option" aria-selected="false">Price: low to high</li>
<li id="opt-price-desc" role="option" aria-selected="false">Price: high to low</li>
<li id="opt-newest" role="option" aria-selected="false">Newest</li>
</ul>
Note the trigger’s aria-labelledby references both the external “Sort by” label and the trigger element itself — this is a documented APG technique for making the accessible name read as “Sort by Featured” (label plus current value) rather than just “Sort by,” so a screen-reader user hears the currently selected option as part of the control’s own name, not only inside the popup.
Keyboard behavior: what has to work without a mouse
The select-only combobox’s keyboard model, per the APG’s worked example:
- Down Arrow, Up Arrow, Enter, Space, Home, or End open the popup when it’s closed.
- Arrow keys move the highlight between options once open; Home/End jump to the first/last option; Page Up/Page Down jump ten options at a time in a long list.
- Typing a printable character jumps the highlight to the next option starting with that letter, the same type-ahead behavior a native
<select>already has. - Enter, Space, or Tab on a highlighted option commits it as the new value and closes the popup.
- Escape closes the popup without changing the value.
WCAG SC 2.1.1 Keyboard (Level A) is the criterion this maps to directly: “all functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes.” A <div>-based dropdown with only a click handler on each option fails this outright — a keyboard user can’t open it, can’t move through the choices, and can’t select one, regardless of how the trigger looks.
Every option needs a real, programmatic name and state
WCAG SC 4.1.2 Name, Role, Value (Level A) requires that “for all user interface components… the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies.” Its own examples name comboboxes directly as custom widgets needing to “programmatically convey their accessible name, their role, and their current state and value.”
In practice, this failure shows up most on a currency or country switcher: the trigger is a <div> with a flag icon and a two-letter code, no accessible name at all, and the popup’s options are <li> elements with onclick handlers and no role="option" — a screen reader announces nothing useful, and there’s no way to tell which option is active without reading the visual text.
A five-minute manual test
Automated scanners will catch a <div> with role="combobox" and no aria-expanded, but they can’t confirm the interaction actually works end to end. A quick manual pass:
- Tab to the dropdown trigger. Confirm it announces its role (“combobox”) and current value (“Sort by Featured”), not just “clickable.”
- Press Enter or Down Arrow to open it. Confirm the popup appears and focus visibly stays on the trigger (or, if using a screen reader, that the highlighted option is announced).
- Arrow through the options. Confirm each one is announced by name, and that typing a letter jumps to a matching option.
- Press Enter on a different option. Confirm the trigger’s displayed value and its accessible name both update to the new selection, and the popup closes.
- Reopen the popup and press Escape. Confirm it closes with the value unchanged.
FAQ
Is a native <select>, restyled with CSS, ever a full replacement for this pattern?
Often, yes — appearance: none plus custom borders, backgrounds, and an icon can restyle the closed control significantly while keeping all of the native keyboard/screen-reader behavior. The custom-built pattern above is for the specific case where the popup’s own appearance (not just the closed control) needs styling the browser doesn’t expose.
Is this the same pattern as the search-autocomplete combobox?
Related but distinct. Both use role="combobox" and a popup role="listbox", but a select-only combobox (this article) has no text input and no filtering — the user picks from a fixed list. An editable combobox (our search-autocomplete article) is a text field where typing filters the options shown.
Does aria-activedescendant need to be updated even if the popup is only a few options long?
Yes — the requirement isn’t about list length, it’s about how the highlighted option gets communicated to assistive technology while real keyboard focus stays on the trigger. A three-option currency switcher needs the same mechanism as a fifty-option country list.
Get your dropdown menus checked by a person, not just a parser
Quietramp’s $890 audits include a manual pass through interactive components like this one — operating them with a keyboard and a real screen reader to confirm the roles, states, and announcements actually work, not just that the right attribute names appear somewhere in the markup. See a real sample report or check pricing — $890 one-time, $99/month for ongoing re-checks after fixes ship.
This is educational content about technical accessibility practice, not legal advice. Meeting WCAG 2.1 AA does not constitute a legal compliance determination under the ADA or any other standard — consult qualified counsel for legal risk questions.
This article was drafted with AI assistance and reviewed by a person for accuracy before publication.