Listbox The option list inside a Popup , with optional groups, search, and multiple selection. Select and Filter chip use it. Scripts own the behavior; CSS styles the state they set.
Installation bun add @harmo-ui/css @harmo-ui/react npm install @harmo-ui/css @harmo-ui/react pnpm add @harmo-ui/css @harmo-ui/react yarn add @harmo-ui/css @harmo-ui/react bunx harmo-ui add listbox pnpm dlx harmo-ui add listbox yarn dlx harmo-ui add listbox Usage import { Button , ExpandChevron , Listbox , Option , Popup } from "@harmo-ui/react" ;
< Popup
trigger = {
< Button variant = "secondary" aria-haspopup = "listbox" >
Period
< ExpandChevron />
</ Button >
}
>
< Listbox aria-label = "Period" defaultValue = "30" >
< Option value = "7" >Last 7 days</ Option >
< Option value = "30" >Last 30 days</ Option >
< Option value = "90" >Last 90 days</ Option >
</ Listbox >
</ Popup >
View full source
Examples Single A role="listbox" of role="option" items inside a Popup. The selected option has aria-selected="true" and a check mark, and stays highlighted while nothing else is. Opening it moves focus to the list: the Arrow keys, Home , and End move the highlight, skipping disabled options, and Enter or Space chooses. Clicking works too.
Period
< Popup
trigger = {
< Button variant = "secondary" aria-haspopup = "listbox" >
Period
< ExpandChevron />
</ Button >
}
>
< Listbox aria-label = "Period" defaultValue = "30" >
< Option value = "7" >Last 7 days</ Option >
< Option value = "30" >Last 30 days</ Option >
< Option value = "90" >Last 90 days</ Option >
< Option value = "year" >This year</ Option >
</ Listbox >
</ Popup >
View full source
< button
class = "hbutton"
data-variant = "secondary"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "period-popup"
style = "anchor-name: --period-popup"
>
< span class = "hbutton-label" >Period</ span >
< svg class = "hexpand-chevron" viewBox = "0 0 16 12" aria-hidden = "true" focusable = "false" >
< g class = "hexpand-chevron-head" >
< line class = "hexpand-chevron-left" x1 = "0" y1 = "6" x2 = "8" y2 = "6" />
< line class = "hexpand-chevron-right" x1 = "16" y1 = "6" x2 = "8" y2 = "6" />
</ g >
</ svg >
</ button >
< div class = "hpopup" id = "period-popup" popover style = "position-anchor: --period-popup" >
< ul class = "hlistbox" role = "listbox" aria-label = "Period" tabindex = "-1" >
< li class = "hoption" id = "period-7" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Last 7 days</ span >
</ li >
< li class = "hoption" id = "period-30" role = "option" aria-selected = "true" >
< span class = "hoption-label" >Last 30 days</ span >
< svg class = "hoption-check" viewBox = "5.845 5.87 14 14" aria-hidden = "true" focusable = "false" >
< path
d = "M11.4261 16.455C11.2261 16.455 11.0361 16.375 10.8961 16.235L8.06613 13.405C7.77613 13.115 7.77613 12.635 8.06613 12.345C8.35613 12.055 8.83613 12.055 9.12613 12.345L11.4261 14.645L16.5661 9.505C16.8561 9.215 17.3361 9.215 17.6261 9.505C17.9161 9.795 17.9161 10.275 17.6261 10.565L11.9561 16.235C11.8161 16.375 11.6261 16.455 11.4261 16.455Z"
fill = "currentColor"
/>
</ svg >
</ li >
< li class = "hoption" id = "period-90" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Last 90 days</ span >
</ li >
< li class = "hoption" id = "period-year" role = "option" aria-selected = "false" >
< span class = "hoption-label" >This year</ span >
</ li >
</ ul >
</ div >
View full source
Single, grouped Groups are role="group" lists labelled by their .hoption-group-label, with a divider above every group after the first. Options outside a group sit above them.
Channel
< Popup
trigger = {
< Button variant = "secondary" aria-haspopup = "listbox" >
Channel
< ExpandChevron />
</ Button >
}
>
< Listbox aria-label = "Channel" defaultValue = "whatsapp" >
< Option value = "all" >All channels</ Option >
< OptionGroup label = "Messaging" >
< Option value = "whatsapp" >WhatsApp</ Option >
< Option value = "telegram" >Telegram</ Option >
< Option value = "viber" >Viber</ Option >
</ OptionGroup >
< OptionGroup label = "Email" >
< Option value = "gmail" >Gmail</ Option >
< Option value = "outlook" >Outlook</ Option >
</ OptionGroup >
</ Listbox >
</ Popup >
View full source
< button
class = "hbutton"
data-variant = "secondary"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "channel-popup"
style = "anchor-name: --channel-popup"
>
< span class = "hbutton-label" >Channel</ span >
< svg class = "hexpand-chevron" viewBox = "0 0 16 12" aria-hidden = "true" focusable = "false" >
< g class = "hexpand-chevron-head" >
< line class = "hexpand-chevron-left" x1 = "0" y1 = "6" x2 = "8" y2 = "6" />
< line class = "hexpand-chevron-right" x1 = "16" y1 = "6" x2 = "8" y2 = "6" />
</ g >
</ svg >
</ button >
< div class = "hpopup" id = "channel-popup" popover style = "position-anchor: --channel-popup" >
< ul class = "hlistbox" role = "listbox" aria-label = "Channel" tabindex = "-1" >
< li class = "hoption" id = "channel-all" role = "option" aria-selected = "false" >
< span class = "hoption-label" >All channels</ span >
</ li >
< li class = "hoption-group" role = "presentation" >
< span class = "hoption-group-label" id = "channel-messaging" >Messaging</ span >
< ul role = "group" aria-labelledby = "channel-messaging" >
< li class = "hoption" id = "channel-whatsapp" role = "option" aria-selected = "true" >
< span class = "hoption-label" >WhatsApp</ span >
< svg class = "hoption-check" viewBox = "5.845 5.87 14 14" aria-hidden = "true" focusable = "false" >
< path
d = "M11.4261 16.455C11.2261 16.455 11.0361 16.375 10.8961 16.235L8.06613 13.405C7.77613 13.115 7.77613 12.635 8.06613 12.345C8.35613 12.055 8.83613 12.055 9.12613 12.345L11.4261 14.645L16.5661 9.505C16.8561 9.215 17.3361 9.215 17.6261 9.505C17.9161 9.795 17.9161 10.275 17.6261 10.565L11.9561 16.235C11.8161 16.375 11.6261 16.455 11.4261 16.455Z"
fill = "currentColor"
/>
</ svg >
</ li >
< li class = "hoption" id = "channel-telegram" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Telegram</ span >
</ li >
< li class = "hoption" id = "channel-viber" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Viber</ span >
</ li >
</ ul >
</ li >
< li class = "hoption-group" role = "presentation" >
< span class = "hoption-group-label" id = "channel-email" >Email</ span >
< ul role = "group" aria-labelledby = "channel-email" >
< li class = "hoption" id = "channel-gmail" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Gmail</ span >
</ li >
< li class = "hoption" id = "channel-outlook" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Outlook</ span >
</ li >
</ ul >
</ li >
</ ul >
</ div >
View full source
Single, with search A .hlistbox-search row above the list. Opening it moves focus to the search field, which keeps it while its aria-activedescendant names the highlighted option. Typing hides the options whose name doesn't contain the text and highlights the first match.
Assignee
< Popup
trigger = {
< Button variant = "secondary" aria-haspopup = "listbox" >
Assignee
< ExpandChevron />
</ Button >
}
>
< Listbox
aria-label = "Assignee"
searchLabel = "Search agents"
emptyMessage = "No agents found"
defaultValue = "jane"
>
< Option value = "none" >Unassigned</ Option >
< Option value = "jane" >Jane Smith</ Option >
< Option value = "omar" >Omar Haddad</ Option >
< Option value = "li" >Li Wei</ Option >
< Option value = "ana" >Ana Souza</ Option >
</ Listbox >
</ Popup >
View full source
< button
class = "hbutton"
data-variant = "secondary"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "assignee-popup"
style = "anchor-name: --assignee-popup"
>
< span class = "hbutton-label" >Assignee</ span >
< svg class = "hexpand-chevron" viewBox = "0 0 16 12" aria-hidden = "true" focusable = "false" >
< g class = "hexpand-chevron-head" >
< line class = "hexpand-chevron-left" x1 = "0" y1 = "6" x2 = "8" y2 = "6" />
< line class = "hexpand-chevron-right" x1 = "16" y1 = "6" x2 = "8" y2 = "6" />
</ g >
</ svg >
</ button >
< div class = "hpopup" id = "assignee-popup" popover style = "position-anchor: --assignee-popup" >
< div class = "hlistbox-search" >
< svg class = "hicon" width = "1.125em" height = "1.125em" viewBox = "0 0 24 24" fill = "none" aria-hidden = "true" focusable = "false" >
< path
d = "M11.5 21.75C5.85 21.75 1.25 17.15 1.25 11.5C1.25 5.85 5.85 1.25 11.5 1.25C17.15 1.25 21.75 5.85 21.75 11.5C21.75 17.15 17.15 21.75 11.5 21.75ZM11.5 2.75C6.67 2.75 2.75 6.68 2.75 11.5C2.75 16.32 6.67 20.25 11.5 20.25C16.33 20.25 20.25 16.32 20.25 11.5C20.25 6.68 16.33 2.75 11.5 2.75Z"
fill = "currentColor"
/>
< path
d = "M21.9995 22.7499C21.8095 22.7499 21.6195 22.6799 21.4695 22.5299L19.4695 20.5299C19.1795 20.2399 19.1795 19.7599 19.4695 19.4699C19.7595 19.1799 20.2395 19.1799 20.5295 19.4699L22.5295 21.4699C22.8195 21.7599 22.8195 22.2399 22.5295 22.5299C22.3795 22.6799 22.1895 22.7499 21.9995 22.7499Z"
fill = "currentColor"
/>
</ svg >
< input
class = "hlistbox-search-input"
type = "text"
role = "combobox"
placeholder = "Search agents"
aria-label = "Search agents"
aria-expanded = "true"
aria-controls = "assignee-listbox"
aria-autocomplete = "list"
/>
</ div >
< ul class = "hlistbox" id = "assignee-listbox" role = "listbox" aria-label = "Assignee" >
< li class = "hoption" id = "assignee-none" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Unassigned</ span >
</ li >
< li class = "hoption" id = "assignee-jane" role = "option" aria-selected = "true" >
< span class = "hoption-label" >Jane Smith</ span >
< svg class = "hoption-check" viewBox = "5.845 5.87 14 14" aria-hidden = "true" focusable = "false" >
< path
d = "M11.4261 16.455C11.2261 16.455 11.0361 16.375 10.8961 16.235L8.06613 13.405C7.77613 13.115 7.77613 12.635 8.06613 12.345C8.35613 12.055 8.83613 12.055 9.12613 12.345L11.4261 14.645L16.5661 9.505C16.8561 9.215 17.3361 9.215 17.6261 9.505C17.9161 9.795 17.9161 10.275 17.6261 10.565L11.9561 16.235C11.8161 16.375 11.6261 16.455 11.4261 16.455Z"
fill = "currentColor"
/>
</ svg >
</ li >
< li class = "hoption" id = "assignee-omar" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Omar Haddad</ span >
</ li >
< li class = "hoption" id = "assignee-li" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Li Wei</ span >
</ li >
< li class = "hoption" id = "assignee-ana" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Ana Souza</ span >
</ li >
</ ul >
</ div >
View full source
Single, grouped, with search Searching a grouped list hides each option that doesn't match, and each group left with none, label and all.
Inbox
< Popup
trigger = {
< Button variant = "secondary" aria-haspopup = "listbox" >
Inbox
< ExpandChevron />
</ Button >
}
>
< Listbox
aria-label = "Inbox"
searchLabel = "Search inboxes"
emptyMessage = "No inboxes found"
defaultValue = "all"
>
< Option value = "all" >All inboxes</ Option >
< OptionGroup label = "Messaging" >
< Option value = "whatsapp" >WhatsApp</ Option >
< Option value = "telegram" >Telegram</ Option >
</ OptionGroup >
< OptionGroup label = "Email" >
< Option value = "gmail" >Gmail</ Option >
< Option value = "outlook" >Outlook</ Option >
</ OptionGroup >
</ Listbox >
</ Popup >
View full source
< button
class = "hbutton"
data-variant = "secondary"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "inbox-popup"
style = "anchor-name: --inbox-popup"
>
< span class = "hbutton-label" >Inbox</ span >
< svg class = "hexpand-chevron" viewBox = "0 0 16 12" aria-hidden = "true" focusable = "false" >
< g class = "hexpand-chevron-head" >
< line class = "hexpand-chevron-left" x1 = "0" y1 = "6" x2 = "8" y2 = "6" />
< line class = "hexpand-chevron-right" x1 = "16" y1 = "6" x2 = "8" y2 = "6" />
</ g >
</ svg >
</ button >
< div class = "hpopup" id = "inbox-popup" popover style = "position-anchor: --inbox-popup" >
< div class = "hlistbox-search" >
< svg class = "hicon" width = "1.125em" height = "1.125em" viewBox = "0 0 24 24" fill = "none" aria-hidden = "true" focusable = "false" >
< path
d = "M11.5 21.75C5.85 21.75 1.25 17.15 1.25 11.5C1.25 5.85 5.85 1.25 11.5 1.25C17.15 1.25 21.75 5.85 21.75 11.5C21.75 17.15 17.15 21.75 11.5 21.75ZM11.5 2.75C6.67 2.75 2.75 6.68 2.75 11.5C2.75 16.32 6.67 20.25 11.5 20.25C16.33 20.25 20.25 16.32 20.25 11.5C20.25 6.68 16.33 2.75 11.5 2.75Z"
fill = "currentColor"
/>
< path
d = "M21.9995 22.7499C21.8095 22.7499 21.6195 22.6799 21.4695 22.5299L19.4695 20.5299C19.1795 20.2399 19.1795 19.7599 19.4695 19.4699C19.7595 19.1799 20.2395 19.1799 20.5295 19.4699L22.5295 21.4699C22.8195 21.7599 22.8195 22.2399 22.5295 22.5299C22.3795 22.6799 22.1895 22.7499 21.9995 22.7499Z"
fill = "currentColor"
/>
</ svg >
< input
class = "hlistbox-search-input"
type = "text"
role = "combobox"
placeholder = "Search inboxes"
aria-label = "Search inboxes"
aria-expanded = "true"
aria-controls = "inbox-listbox"
aria-autocomplete = "list"
/>
</ div >
< ul class = "hlistbox" id = "inbox-listbox" role = "listbox" aria-label = "Inbox" >
< li class = "hoption" id = "inbox-all" role = "option" aria-selected = "true" >
< span class = "hoption-label" >All inboxes</ span >
< svg class = "hoption-check" viewBox = "5.845 5.87 14 14" aria-hidden = "true" focusable = "false" >
< path
d = "M11.4261 16.455C11.2261 16.455 11.0361 16.375 10.8961 16.235L8.06613 13.405C7.77613 13.115 7.77613 12.635 8.06613 12.345C8.35613 12.055 8.83613 12.055 9.12613 12.345L11.4261 14.645L16.5661 9.505C16.8561 9.215 17.3361 9.215 17.6261 9.505C17.9161 9.795 17.9161 10.275 17.6261 10.565L11.9561 16.235C11.8161 16.375 11.6261 16.455 11.4261 16.455Z"
fill = "currentColor"
/>
</ svg >
</ li >
< li class = "hoption-group" role = "presentation" >
< span class = "hoption-group-label" id = "inbox-messaging" >Messaging</ span >
< ul role = "group" aria-labelledby = "inbox-messaging" >
< li class = "hoption" id = "inbox-whatsapp" role = "option" aria-selected = "false" >
< span class = "hoption-label" >WhatsApp</ span >
</ li >
< li class = "hoption" id = "inbox-telegram" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Telegram</ span >
</ li >
</ ul >
</ li >
< li class = "hoption-group" role = "presentation" >
< span class = "hoption-group-label" id = "inbox-email" >Email</ span >
< ul role = "group" aria-labelledby = "inbox-email" >
< li class = "hoption" id = "inbox-gmail" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Gmail</ span >
</ li >
< li class = "hoption" id = "inbox-outlook" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Outlook</ span >
</ li >
</ ul >
</ li >
</ ul >
</ div >
View full source
Multiple aria-multiselectable="true" on the list, and a .hoption-checkbox at the start of each option instead of the check mark. Clicking toggles an option. The checkbox is a drawing, not an input, so nothing inside the list takes focus.
Status
< Popup
trigger = {
< Button variant = "secondary" aria-haspopup = "listbox" >
Status
< ExpandChevron />
</ Button >
}
>
< Listbox aria-label = "Status" multiple defaultValue = { [ "delivered" , "read" ] } >
< Option value = "delivered" >Delivered</ Option >
< Option value = "read" >Read</ Option >
< Option value = "failed" >Failed</ Option >
< Option value = "pending" >Pending</ Option >
</ Listbox >
</ Popup >
View full source
< button
class = "hbutton"
data-variant = "secondary"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "status-popup"
style = "anchor-name: --status-popup"
>
< span class = "hbutton-label" >Status</ span >
< svg class = "hexpand-chevron" viewBox = "0 0 16 12" aria-hidden = "true" focusable = "false" >
< g class = "hexpand-chevron-head" >
< line class = "hexpand-chevron-left" x1 = "0" y1 = "6" x2 = "8" y2 = "6" />
< line class = "hexpand-chevron-right" x1 = "16" y1 = "6" x2 = "8" y2 = "6" />
</ g >
</ svg >
</ button >
< div class = "hpopup" id = "status-popup" popover style = "position-anchor: --status-popup" >
< ul class = "hlistbox" role = "listbox" aria-label = "Status" aria-multiselectable = "true" tabindex = "-1" >
< li class = "hoption" id = "status-delivered" role = "option" aria-selected = "true" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Delivered</ span >
</ li >
< li class = "hoption" id = "status-read" role = "option" aria-selected = "true" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Read</ span >
</ li >
< li class = "hoption" id = "status-failed" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Failed</ span >
</ li >
< li class = "hoption" id = "status-pending" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Pending</ span >
</ li >
</ ul >
</ div >
View full source
Multiple, grouped Groups work the same way in a multi-select.
Tags
< Popup
trigger = {
< Button variant = "secondary" aria-haspopup = "listbox" >
Tags
< ExpandChevron />
</ Button >
}
>
< Listbox aria-label = "Tags" multiple defaultValue = { [ "refund" , "feature" ] } >
< OptionGroup label = "Billing" >
< Option value = "refund" >Refund</ Option >
< Option value = "invoice" >Invoice</ Option >
</ OptionGroup >
< OptionGroup label = "Support" >
< Option value = "bug" >Bug report</ Option >
< Option value = "feature" >Feature request</ Option >
< Option value = "onboarding" >Onboarding</ Option >
</ OptionGroup >
</ Listbox >
</ Popup >
View full source
< button
class = "hbutton"
data-variant = "secondary"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "tags-popup"
style = "anchor-name: --tags-popup"
>
< span class = "hbutton-label" >Tags</ span >
< svg class = "hexpand-chevron" viewBox = "0 0 16 12" aria-hidden = "true" focusable = "false" >
< g class = "hexpand-chevron-head" >
< line class = "hexpand-chevron-left" x1 = "0" y1 = "6" x2 = "8" y2 = "6" />
< line class = "hexpand-chevron-right" x1 = "16" y1 = "6" x2 = "8" y2 = "6" />
</ g >
</ svg >
</ button >
< div class = "hpopup" id = "tags-popup" popover style = "position-anchor: --tags-popup" >
< ul class = "hlistbox" role = "listbox" aria-label = "Tags" aria-multiselectable = "true" tabindex = "-1" >
< li class = "hoption-group" role = "presentation" >
< span class = "hoption-group-label" id = "tags-billing" >Billing</ span >
< ul role = "group" aria-labelledby = "tags-billing" >
< li class = "hoption" id = "tag-refund" role = "option" aria-selected = "true" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Refund</ span >
</ li >
< li class = "hoption" id = "tag-invoice" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Invoice</ span >
</ li >
</ ul >
</ li >
< li class = "hoption-group" role = "presentation" >
< span class = "hoption-group-label" id = "tags-support" >Support</ span >
< ul role = "group" aria-labelledby = "tags-support" >
< li class = "hoption" id = "tag-bug" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Bug report</ span >
</ li >
< li class = "hoption" id = "tag-feature" role = "option" aria-selected = "true" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Feature request</ span >
</ li >
< li class = "hoption" id = "tag-onboarding" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Onboarding</ span >
</ li >
</ ul >
</ li >
</ ul >
</ div >
View full source
A .hlistbox-footer below the list holds a full-width action, such as clearing the filter. Here it clears the selection.
Language
const [ languages , setLanguages ] = useState ([ "en" , "es" ]);
return (
< Popup
trigger = {
< Button variant = "secondary" aria-haspopup = "listbox" >
Language
< ExpandChevron />
</ Button >
}
>
< Listbox
aria-label = "Language"
multiple
value = { languages }
onValueChange = { setLanguages }
footer = {
< Button variant = "tertiary" onClick = { () => setLanguages ([]) } >
Clear filters
</ Button >
}
>
< Option value = "en" >English</ Option >
< Option value = "es" >Spanish</ Option >
< Option value = "ar" >Arabic</ Option >
< Option value = "de" >German</ Option >
</ Listbox >
</ Popup >
);
View full source
< button
class = "hbutton"
data-variant = "secondary"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "language-popup"
style = "anchor-name: --language-popup"
>
< span class = "hbutton-label" >Language</ span >
< svg class = "hexpand-chevron" viewBox = "0 0 16 12" aria-hidden = "true" focusable = "false" >
< g class = "hexpand-chevron-head" >
< line class = "hexpand-chevron-left" x1 = "0" y1 = "6" x2 = "8" y2 = "6" />
< line class = "hexpand-chevron-right" x1 = "16" y1 = "6" x2 = "8" y2 = "6" />
</ g >
</ svg >
</ button >
< div class = "hpopup" id = "language-popup" popover style = "position-anchor: --language-popup" >
< ul class = "hlistbox" role = "listbox" aria-label = "Language" aria-multiselectable = "true" tabindex = "-1" >
< li class = "hoption" id = "language-en" role = "option" aria-selected = "true" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >English</ span >
</ li >
< li class = "hoption" id = "language-es" role = "option" aria-selected = "true" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Spanish</ span >
</ li >
< li class = "hoption" id = "language-ar" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Arabic</ span >
</ li >
< li class = "hoption" id = "language-de" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >German</ span >
</ li >
</ ul >
< div class = "hlistbox-footer" >
< button class = "hbutton" data-variant = "tertiary" type = "button" >
< span class = "hbutton-label" >Clear filters</ span >
</ button >
</ div >
</ div >
View full source
Disabled options aria-disabled="true" grays an option and leaves it out of the highlight and selection. A .hoption-badge after the label says why, such as Soon.
Source
< Popup
trigger = {
< Button variant = "secondary" aria-haspopup = "listbox" >
Source
< ExpandChevron />
</ Button >
}
>
< Listbox aria-label = "Source" defaultValue = "whatsapp" >
< Option value = "whatsapp" >WhatsApp</ Option >
< Option value = "instagram" >Instagram</ Option >
< Option value = "viber" disabled badge = "Soon" >
Viber
</ Option >
< Option value = "sms" disabled >
SMS
</ Option >
</ Listbox >
</ Popup >
View full source
< button
class = "hbutton"
data-variant = "secondary"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "source-popup"
style = "anchor-name: --source-popup"
>
< span class = "hbutton-label" >Source</ span >
< svg class = "hexpand-chevron" viewBox = "0 0 16 12" aria-hidden = "true" focusable = "false" >
< g class = "hexpand-chevron-head" >
< line class = "hexpand-chevron-left" x1 = "0" y1 = "6" x2 = "8" y2 = "6" />
< line class = "hexpand-chevron-right" x1 = "16" y1 = "6" x2 = "8" y2 = "6" />
</ g >
</ svg >
</ button >
< div class = "hpopup" id = "source-popup" popover style = "position-anchor: --source-popup" >
< ul class = "hlistbox" role = "listbox" aria-label = "Source" tabindex = "-1" >
< li class = "hoption" id = "source-whatsapp" role = "option" aria-selected = "true" >
< span class = "hoption-label" >WhatsApp</ span >
< svg class = "hoption-check" viewBox = "5.845 5.87 14 14" aria-hidden = "true" focusable = "false" >
< path
d = "M11.4261 16.455C11.2261 16.455 11.0361 16.375 10.8961 16.235L8.06613 13.405C7.77613 13.115 7.77613 12.635 8.06613 12.345C8.35613 12.055 8.83613 12.055 9.12613 12.345L11.4261 14.645L16.5661 9.505C16.8561 9.215 17.3361 9.215 17.6261 9.505C17.9161 9.795 17.9161 10.275 17.6261 10.565L11.9561 16.235C11.8161 16.375 11.6261 16.455 11.4261 16.455Z"
fill = "currentColor"
/>
</ svg >
</ li >
< li class = "hoption" id = "source-instagram" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Instagram</ span >
</ li >
< li class = "hoption" id = "source-viber" role = "option" aria-selected = "false" aria-disabled = "true" >
< span class = "hoption-label" >Viber</ span > < span class = "hoption-badge" >Soon</ span >
</ li >
< li class = "hoption" id = "source-sms" role = "option" aria-selected = "false" aria-disabled = "true" >
< span class = "hoption-label" >SMS</ span >
</ li >
</ ul >
</ div >
View full source
Long options Options stay one line. A long one widens the list up to 32rem; beyond that it ends with an ellipsis, and hovering it shows the full text.
Sender number
< Popup
trigger = {
< Button variant = "secondary" aria-haspopup = "listbox" >
Sender number
< ExpandChevron />
</ Button >
}
>
< Listbox aria-label = "Sender number" defaultValue = "support" >
< Option value = "support" >+49 30 1234567 · Support</ Option >
< Option value = "dach" >
+41 44 7654321 · Customer support for Germany, Austria, Switzerland,
Liechtenstein, and Luxembourg
</ Option >
< Option value = "sales" >+44 20 7946 0000 · Sales</ Option >
</ Listbox >
</ Popup >
View full source
< button
class = "hbutton"
data-variant = "secondary"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "number-popup"
style = "anchor-name: --number-popup"
>
< span class = "hbutton-label" >Sender number</ span >
< svg class = "hexpand-chevron" viewBox = "0 0 16 12" aria-hidden = "true" focusable = "false" >
< g class = "hexpand-chevron-head" >
< line class = "hexpand-chevron-left" x1 = "0" y1 = "6" x2 = "8" y2 = "6" />
< line class = "hexpand-chevron-right" x1 = "16" y1 = "6" x2 = "8" y2 = "6" />
</ g >
</ svg >
</ button >
< div class = "hpopup" id = "number-popup" popover style = "position-anchor: --number-popup" >
< ul class = "hlistbox" role = "listbox" aria-label = "Sender number" tabindex = "-1" >
< li class = "hoption" id = "number-support" role = "option" aria-selected = "true" >
< span class = "hoption-label" >+49 30 1234567 · Support</ span >
< svg class = "hoption-check" viewBox = "5.845 5.87 14 14" aria-hidden = "true" focusable = "false" >
< path
d = "M11.4261 16.455C11.2261 16.455 11.0361 16.375 10.8961 16.235L8.06613 13.405C7.77613 13.115 7.77613 12.635 8.06613 12.345C8.35613 12.055 8.83613 12.055 9.12613 12.345L11.4261 14.645L16.5661 9.505C16.8561 9.215 17.3361 9.215 17.6261 9.505C17.9161 9.795 17.9161 10.275 17.6261 10.565L11.9561 16.235C11.8161 16.375 11.6261 16.455 11.4261 16.455Z"
fill = "currentColor"
/>
</ svg >
</ li >
< li class = "hoption" id = "number-dach" role = "option" aria-selected = "false" >
< span class = "hoption-label" >
+41 44 7654321 · Customer support for Germany, Austria, Switzerland, Liechtenstein, and Luxembourg
</ span >
</ li >
< li class = "hoption" id = "number-sales" role = "option" aria-selected = "false" >
< span class = "hoption-label" >+44 20 7946 0000 · Sales</ span >
</ li >
</ ul >
</ div >
View full source
API reference React <Listbox> renders the list, with <Option> and <OptionGroup> children, and does everything a script would. Put it in a Popup , and name it with aria-label or aria-labelledby. Other props are the <ul>'s attributes.
<Option> takes its value, its label as children, and optionally disabled and a badge, such as "Soon". <OptionGroup> takes a label. Other props are the <li>'s attributes: an id replaces the generated one, and hidden hides the option as a search does.
HTML A <ul class="hlistbox" role="listbox"> inside a .hpopup, with aria-label or aria-labelledby. Each .hoption has role="option", an id, and aria-selected. Load listbox.css and popup.css; button.css for a footer or a button that opens it.
To filter, set hidden on each option that doesn't match, and on a group left with none; the options stay in the page, selection included. When nothing matches, render .hlistbox-empty after the list, and only then. A list with every option hidden, or with nothing inside it (not even whitespace), loses its padding and collapses:
< ul class = "hlistbox" id = "channel-listbox" role = "listbox" aria-label = "Channel" ></ ul >
< p class = "hlistbox-empty" >No channels found</ p > It needs a script for the highlight, keyboard, selection, and search. See Listbox's full contract .
Accessibility role="listbox" with role="option" items; groups are role="group" labelled by their .hoption-group-label.Focus stays on one element while the list is open, and its aria-activedescendant names the highlighted option, so screen readers announce it as the highlight moves: a Select trigger keeps focus; a list with a search row moves it to the search field; otherwise the list itself takes focus (with tabindex="-1"). Keys: Down and Up move the highlight, Home and End go to the first and last option, and typing jumps to the next option whose label starts with the typed letters (except in a search field). Enter chooses; in a multi-select, Space toggles. Disabled options are skipped. With nothing to choose, Enter in the search field does nothing, so it can't submit the form around it. The search field needs no focus ring: its caret shows focus.