Skip to content
Home Theme Gallery

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

Select
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
Attributes
NameTypeDefaultDescription
namestring""The form field name the selected value submits under. Reflected, inherited from the form-associated base.
valuestring""The selected option's data-value (or value). Reflected.
placeholderstring""Shown in the trigger until a value is selected. Reflected.
requiredbooleanfalseBlocks form submission with valueMissing until a value is chosen. Reflected.
disabledbooleanfalseDisables the trigger and prevents the listbox opening. Reflected, inherited from the form-associated base.
openbooleanfalseReflected 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
Properties
NameTypeDefaultDescription
namestring''Reflects to the name attribute.
valuestring''Reflects to the value attribute. Single mode only.
valuesstring[]-Read-only, multiple mode. The selected values in document order, read from the options' aria-selected rather than kept on the element.
placeholderstring''Reflects to the placeholder attribute.
requiredbooleanfalseReflects to the required attribute.
disabledbooleanfalseReflects to the disabled attribute.
openbooleanfalseReflects to the open attribute.
placement'top' | 'bottom''bottom'Reflects to the placement attribute.
selection'single' | 'multiple''single'Reflects to the selection attribute.
Events5
Events
NameDescription
hui-openThe listbox entered the top layer. It bubbles and is composed, and carries no detail.
hui-closeThe listbox left the top layer. It bubbles and is composed, and carries no detail.
inputA 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.
changeA 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
Slots
NameDescription
(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
CSS custom properties
NameDefaultDescription
--hui-backgroundoklch(1 0 0)Trigger background.
--hui-inputoklch(0.62 0.019 323.02)Trigger border.
--hui-popoveroklch(1 0 0)Panel background.
--hui-accentoklch(0.96 0.003 325.6)Active option background.
--hui-radius-mdcalc(var(--hui-radius, 0.45rem) * 0.8)Trigger and panel corner radius.
::part() hooks6
::part() hooks
NameDescription
triggerThe internal role="combobox" button.
valueThe selected label, hidden until a value is chosen.
placeholderThe placeholder text, hidden once a value is chosen.
chevronThe arrow icon, rotated while the listbox is open.
panelThe native popover panel in the top layer.
listboxThe 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.

Preselected
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.

Disabled option and required
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.

Multiple
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.

Groups
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" with aria-haspopup="listbox", aria-expanded and aria-controls; focus stays on it while open, with aria-activedescendant naming the active option.
  • The options are role="option" with aria-selected; the active one is marked data-active="true".
  • Form-associated: it submits one value under name, participates in FormData, restores the value on form reset, and reports valueMissing when required and 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 its label or aria-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 disabled or aria-disabled="true" and are dimmed and never chosen.

Keyboard

Keyboard
KeysAction
Arrow Down, Arrow Up, Enter, SpaceWhile closed, open the listbox; Enter and Space also open it without moving the active option.
Arrow Down, Arrow Up, Home, EndWhile 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.
EscapeClose the listbox and keep the current value.
Printable charactersType-ahead moves the active option to the first label starting with the typed buffer.

Gotchas

  • placement is 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.