Select
The custom element is <hui-select>.
Overview
A listbox in a native popover that submits under one name, exactly as a native <select> would. It combines the overlay layer with ElementInternals, is the base for hui-combobox, and takes a selection="multiple" mode and declared option groups.
Example
Source
<hui-select name="fruit" placeholder="Pick a fruit" aria-label="Fruit"> <div role="option" data-value="apple">Apple</div> <div role="option" data-value="banana">Banana</div> <div role="option" data-value="cherry">Cherry</div></hui-select>The Go template that renders it:
<form hx-post="/search"> <hui-select name="fruit" value="{{ .Fruit }}" placeholder="Pick a fruit" aria-label="Fruit"> {{ range .Fruits }} <div role="option" data-value="{{ .ID }}">{{ .Label }}</div> {{ end }} </hui-select></form>API
Attributes8
| Name | Type | Default | Description |
|---|---|---|---|
name | string | "" | The form field name the selected value submits under. Reflected, inherited from the form-associated base. |
value | string | "" | The selected option's data-value (or value). Reflected. |
placeholder | string | "" | Shown in the trigger until a value is selected. Reflected. |
required | boolean | false | Blocks form submission with valueMissing until a value is chosen. Reflected. |
disabled | boolean | false | Disables the trigger and prevents the listbox opening. Reflected, inherited from the form-associated base. |
open | boolean | false | Reflected listbox open state; setting it promotes or hides the native popover. |
placement | "top" | "bottom" | "bottom" | Opens the panel below by default; top flips it above the trigger. |
selection | "single" | "multiple" | "single" | A single value that closes on choice, or multiple values over the shared cursor/range model that keep the list open. In multiple the selection is the options' aria-selected, submitted as a repeated field. |
Properties9
| Name | Type | Default | Description |
|---|---|---|---|
name | string | '' | Reflects to the name attribute. |
value | string | '' | Reflects to the value attribute. Single mode only. |
values | string[] | - | Read-only, multiple mode. The selected values in document order, read from the options' aria-selected rather than kept on the element. |
placeholder | string | '' | Reflects to the placeholder attribute. |
required | boolean | false | Reflects to the required attribute. |
disabled | boolean | false | Reflects to the disabled attribute. |
open | boolean | false | Reflects to the open attribute. |
placement | 'top' | 'bottom' | 'bottom' | Reflects to the placement attribute. |
selection | 'single' | 'multiple' | 'single' | Reflects to the selection attribute. |
Events5
| Name | Description |
|---|---|
hui-open | The listbox entered the top layer. It bubbles and is composed, and carries no detail. |
hui-close | The listbox left the top layer. It bubbles and is composed, and carries no detail. |
input | A native-named event dispatched on selection, so Alpine x-model binds without an adapter. It bubbles and is composed. In multiple mode its detail is the values array, as hui-listbox does. |
change | A native-named event dispatched after input on selection. It bubbles and is composed. |
click on [part="trigger"] | Opens the listbox when closed and closes it when open; disabled ignores the click. |
Slots1
| Name | Description |
|---|---|
(default) | The role="option" children, directly or nested in a role="group" with a label. Each option carries data-value (or value) and optional data-label. |
CSS custom properties5
| Name | Default | Description |
|---|---|---|
--hui-background | oklch(1 0 0) | Trigger background. |
--hui-input | oklch(0.62 0.019 323.02) | Trigger border. |
--hui-popover | oklch(1 0 0) | Panel background. |
--hui-accent | oklch(0.96 0.003 325.6) | Active option background. |
--hui-radius-md | calc(var(--hui-radius, 0.45rem) * 0.8) | Trigger and panel corner radius. |
::part() hooks6
| Name | Description |
|---|---|
trigger | The internal role="combobox" button. |
value | The selected label, hidden until a value is chosen. |
placeholder | The placeholder text, hidden once a value is chosen. |
chevron | The arrow icon, rotated while the listbox is open. |
panel | The native popover panel in the top layer. |
listbox | The internal role="listbox" wrapper inside the panel. |
States
Preselected
The server rendering a chosen value: value matches an option's data-value, and the trigger shows that option's text.
Source
<hui-select name="fruit" value="banana" aria-label="Fruit"> <div role="option" data-value="apple">Apple</div> <div role="option" data-value="banana">Banana</div> <div role="option" data-value="cherry">Cherry</div></hui-select>Disabled option and required
Inside a form: required holds up submission until something is chosen, and an option marked aria-disabled="true" stays listed and announced but cannot be picked.
Source
<form> <hui-select required name="fruit" placeholder="Required" aria-label="Fruit"> <div role="option" data-value="apple">Apple</div> <div role="option" data-value="pear" aria-disabled="true">Pear</div> </hui-select></form>Multiple
selection="multiple" keeps the listbox open across choices, so two values take one opening. Plain click replaces the selection as a native multiple select does, Ctrl or Cmd toggles, Shift extends a range, and Ctrl+A selects every enabled option. The server carries the selection on the options' aria-selected, and the form submits one repeated field per value. The trigger shows the value when one is chosen and a count otherwise, and the count is in the accessible name.
Source
<hui-select selection="multiple" name="tags" aria-label="Tags"> <div role="option" data-value="red" aria-selected="true">Red</div> <div role="option" data-value="green" aria-selected="true">Green</div> <div role="option" data-value="blue">Blue</div></hui-select>Groups
Options may be nested in role="group" elements. A group carries a label (or aria-label) that the select turns into the group's accessible name, so a screen reader announces "Europe" before Paris and Rome; a [data-group-label] child is wired by aria-labelledby instead. The cursor still walks every option in document order.
Source
<hui-select name="city" placeholder="Pick a city" aria-label="City"> <div role="group" label="Europe"> <div role="option" data-value="paris">Paris</div> <div role="option" data-value="rome">Rome</div> </div> <div role="group" label="Asia"> <div role="option" data-value="tokyo">Tokyo</div> </div></hui-select>Server-side mechanics
The server owns the options and the state attributes: role="option" children carrying data-value (or value), plus name, value, placeholder, required and disabled. Render <hui-select name="fruit" value="banana"> to preselect an option from the first frame, and read value and open back because they reflect. This is the one form-associated overlay: it submits exactly one value under its name, participates in FormData, restores its value on form reset and reports valueMissing when required; with no name, or while disabled, it submits nothing. An hx-swap that replaces the select while the listbox is open removes it and disconnectedCallback() hides the popover, so nothing is stranded in the top layer, and a swap that replaces the options re-syncs selection and ids. A swap that re-renders the element mid-interaction gives whatever the server echoed, so emit value and open to keep the reader where they were.
Accessibility
- The trigger is
role="combobox"witharia-haspopup="listbox",aria-expandedandaria-controls; focus stays on it while open, witharia-activedescendantnaming the active option. - The options are
role="option"witharia-selected; the active one is markeddata-active="true". - Form-associated: it submits one value under
name, participates inFormData, restores the value on form reset, and reportsvalueMissingwhenrequiredand empty. In multiple mode it submits one repeated field per selected value, as a native<select multiple>does. - In multiple mode the trigger shows the value when one is chosen and a count otherwise, and the count is part of the accessible name even when the host supplies its own
aria-label. - A
role="group"option group is named from itslabeloraria-label, so a reader hears the group before its options. - Options are not focusable; mousedown on the panel is prevented so the trigger keeps focus and the select-only combobox pattern holds.
- Disabled options are marked
disabledoraria-disabled="true"and are dimmed and never chosen.
Keyboard
| Keys | Action |
|---|---|
Arrow Down, Arrow Up, Enter, Space | While closed, open the listbox; Enter and Space also open it without moving the active option. |
Arrow Down, Arrow Up, Home, End | While open, move the active option, wrapping and skipping disabled options. |
Enter, Space (single) | Select the active option, emit input and change, and close. |
Space (multiple) | Toggle the active option without closing, so several can be chosen in one opening. |
Ctrl or Cmd + A (multiple) | Select every enabled option. |
Escape | Close the listbox and keep the current value. |
Printable characters | Type-ahead moves the active option to the first label starting with the typed buffer. |
Gotchas
placementis typed'top' | 'bottom'but is never normalised, unlike the popover base: any other value is reflected verbatim and silently falls back to the default below-trigger position, so a typo neither errors nor flips the panel.