Overview of the Phoenix and LiveView Headless Select Component
The Mishka Chelekom Headless Select
ships zero opinionated styling
- only the semantic markup, full WAI-ARIA
wiring, and paired-presence state
(
data-open
/
data-closed
).
One engine drives the whole component: the trigger
(role="combobox") opens the popup
listbox; single-select commits and closes, multiple
toggles items and stays open. Arrow Up/Down and Home/End move a roving highlight,
Enter/Space commit, Escape/Tab close, and typeahead jumps to a matching option. The
selection mirrors into hidden inputs for form submission. Every part - label, trigger, value, icon, positioner, popup, group,
item
and item-indicator
- is addressable through a
*_class
attribute, so you style it entirely with your own Tailwind
classes while the accessibility and behavior keep working underneath.
Base UI Select example - single select (Phoenix headless)
- Gala
- Fuji
- Honeycrisp
- Granny Smith
- Pink Lady
<.select id="baseui-select-hero" label="Apple" placeholder="Select apple" class="flex flex-col items-start gap-1" label_class="cursor-default text-sm font-bold text-neutral-950 dark:text-white" trigger_class="flex h-8 min-w-40 items-center justify-between gap-3 pl-2 pr-1 text-sm leading-none whitespace-nowrap border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 text-neutral-950 dark:text-white select-none hover:not-data-[disabled]:bg-neutral-100 dark:hover:not-data-[disabled]:bg-neutral-800 active:not-data-[disabled]:bg-neutral-200 dark:active:not-data-[disabled]:bg-neutral-700 data-[disabled]:border-neutral-500 data-[disabled]:text-neutral-500 disabled:border-neutral-500 disabled:text-neutral-500 dark:data-[disabled]:border-neutral-400 dark:data-[disabled]:text-neutral-400 data-[popup-open]:bg-neutral-100 dark:data-[popup-open]:bg-neutral-800 font-normal focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white" value_class="data-[placeholder]:text-neutral-500 dark:data-[placeholder]:text-neutral-400" icon_class="flex items-center" positioner_class="outline-hidden select-none z-10" popup_class="group min-w-[var(--anchor-width)] origin-[var(--transform-origin)] py-1 bg-clip-padding border border-neutral-950 bg-white text-neutral-950 outline-hidden shadow-[0.25rem_0.25rem_0] shadow-black/12 transition-[scale,opacity] duration-100 ease-out data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[side=none]:translate-y-px data-[side=none]:min-w-[calc(var(--anchor-width)+1.75rem)] data-[side=none]:data-[ending-style]:transition-none data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0 data-[side=none]:data-[starting-style]:scale-100 data-[side=none]:data-[starting-style]:opacity-100 data-[side=none]:data-[starting-style]:transition-none dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none" item_class="grid cursor-default grid-cols-[1rem_1fr] items-center gap-2 py-1.5 pr-4 pl-2.5 text-sm outline-hidden select-none data-[highlighted]:bg-neutral-950 data-[highlighted]:text-white dark:data-[highlighted]:bg-white dark:data-[highlighted]:text-neutral-950" item_indicator_class="col-start-1" item_text_class="col-start-2" > <:icon> <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" class="block"> <path d="M11 10H5l3 3.5zm0-4H5l3-3.5z" /> </svg> </:icon> <:item_indicator> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" class="block" > <path d="m2.5 8.5 4 4 7-9" /> </svg> </:item_indicator> <:option value="gala">Gala</:option> <:option value="fuji">Fuji</:option> <:option value="honeycrisp">Honeycrisp</:option> <:option value="granny-smith">Granny Smith</:option> <:option value="pink-lady">Pink Lady</:option> </.select>
Base UI Select example - grouped options (Phoenix headless)
-
Fruits
- Apple
- Banana
- Mango
- Kiwi
- Grape
- Orange
- Strawberry
- Watermelon
-
Vegetables
- Broccoli
- Carrot
- Cauliflower
- Cucumber
- Kale
- Bell pepper
- Spinach
- Zucchini
<.select id="baseui-select-grouped" label="Produce" placeholder="Select produce" class="flex flex-col items-start gap-1" label_class="cursor-default text-sm font-bold text-neutral-950 dark:text-white" trigger_class="flex h-8 min-w-44 items-center justify-between gap-3 pl-2 pr-1 text-sm leading-none whitespace-nowrap border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 text-neutral-950 dark:text-white select-none hover:not-data-[disabled]:bg-neutral-100 dark:hover:not-data-[disabled]:bg-neutral-800 active:not-data-[disabled]:bg-neutral-200 dark:active:not-data-[disabled]:bg-neutral-700 data-[disabled]:border-neutral-500 data-[disabled]:text-neutral-500 disabled:border-neutral-500 disabled:text-neutral-500 dark:data-[disabled]:border-neutral-400 dark:data-[disabled]:text-neutral-400 data-[popup-open]:bg-neutral-100 dark:data-[popup-open]:bg-neutral-800 font-normal focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white" value_class="data-[placeholder]:text-neutral-500 dark:data-[placeholder]:text-neutral-400" icon_class="flex items-center" positioner_class="outline-hidden select-none z-10" popup_class="group min-w-[var(--anchor-width)] origin-[var(--transform-origin)] py-1 bg-clip-padding border border-neutral-950 bg-white text-neutral-950 outline-hidden shadow-[0.25rem_0.25rem_0] shadow-black/12 transition-[scale,opacity] duration-100 ease-out data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[side=none]:translate-y-px data-[side=none]:min-w-[calc(var(--anchor-width)+1.75rem)] data-[side=none]:data-[ending-style]:transition-none data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0 data-[side=none]:data-[starting-style]:scale-100 data-[side=none]:data-[starting-style]:opacity-100 data-[side=none]:data-[starting-style]:transition-none dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none" group_class="block pb-0.5 last:pb-0" group_label_class="py-1.5 pr-4 pl-[2.125rem] text-sm leading-5 text-neutral-500 select-none dark:text-neutral-400" item_class="grid cursor-default grid-cols-[1rem_1fr] items-center gap-2 py-1.5 pr-4 pl-2.5 text-sm outline-hidden select-none group-data-[side=none]:pr-12 data-[highlighted]:bg-neutral-950 data-[highlighted]:text-white dark:data-[highlighted]:bg-white dark:data-[highlighted]:text-neutral-950" item_indicator_class="col-start-1" item_text_class="col-start-2" > <:icon> <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" class="block"> <path d="M11 10H5l3 3.5zm0-4H5l3-3.5z" /> </svg> </:icon> <:item_indicator> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" class="block" > <path d="m2.5 8.5 4 4 7-9" /> </svg> </:item_indicator> <:option value="apple" group="Fruits">Apple</:option> <:option value="banana" group="Fruits">Banana</:option> <:option value="mango" group="Fruits">Mango</:option> <:option value="kiwi" group="Fruits">Kiwi</:option> <:option value="grape" group="Fruits">Grape</:option> <:option value="orange" group="Fruits">Orange</:option> <:option value="strawberry" group="Fruits">Strawberry</:option> <:option value="watermelon" group="Fruits">Watermelon</:option> <:option value="broccoli" group="Vegetables">Broccoli</:option> <:option value="carrot" group="Vegetables">Carrot</:option> <:option value="cauliflower" group="Vegetables">Cauliflower</:option> <:option value="cucumber" group="Vegetables">Cucumber</:option> <:option value="kale" group="Vegetables">Kale</:option> <:option value="pepper" group="Vegetables">Bell pepper</:option> <:option value="spinach" group="Vegetables">Spinach</:option> <:option value="zucchini" group="Vegetables">Zucchini</:option> </.select>
Base UI Select example - multiple selection (Phoenix headless)
- JavaScript
- TypeScript
- Python
- Java
- C#
- PHP
- C++
- Rust
- Go
- Swift
<.select id="baseui-select-multiple" label="Languages" multiple value={["javascript", "typescript"]} placeholder="Select languages" class="flex flex-col items-start gap-1" label_class="cursor-default text-sm font-bold text-neutral-950 dark:text-white" trigger_class="flex h-8 min-w-[14rem] items-center justify-between gap-3 pl-2 pr-1 text-sm leading-none whitespace-nowrap border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 text-neutral-950 dark:text-white select-none hover:not-data-[disabled]:bg-neutral-100 dark:hover:not-data-[disabled]:bg-neutral-800 active:not-data-[disabled]:bg-neutral-200 dark:active:not-data-[disabled]:bg-neutral-700 data-[disabled]:border-neutral-500 data-[disabled]:text-neutral-500 disabled:border-neutral-500 disabled:text-neutral-500 dark:data-[disabled]:border-neutral-400 dark:data-[disabled]:text-neutral-400 data-[popup-open]:bg-neutral-100 dark:data-[popup-open]:bg-neutral-800 font-normal focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white" value_class="data-[placeholder]:text-neutral-500 dark:data-[placeholder]:text-neutral-400" icon_class="flex items-center" positioner_class="outline-hidden z-10" popup_class="group max-h-[var(--available-height)] min-w-[var(--anchor-width)] origin-[var(--transform-origin)] bg-clip-padding overflow-y-auto border border-neutral-950 bg-white py-1 text-neutral-950 outline-hidden shadow-[0.25rem_0.25rem_0] shadow-black/12 transition-[scale,opacity] duration-100 ease-out data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[side=none]:min-w-[calc(var(--anchor-width)+1.75rem)] data-[side=none]:data-[ending-style]:transition-none data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0 data-[side=none]:data-[starting-style]:scale-100 data-[side=none]:data-[starting-style]:opacity-100 data-[side=none]:data-[starting-style]:transition-none dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none" item_class="grid cursor-default grid-cols-[1rem_1fr] items-center gap-2 py-1.5 pr-2.5 pl-2.5 text-sm outline-hidden select-none scroll-my-1 [@media(hover:hover)]:data-[highlighted]:bg-neutral-950 [@media(hover:hover)]:data-[highlighted]:text-white dark:[@media(hover:hover)]:data-[highlighted]:bg-white dark:[@media(hover:hover)]:data-[highlighted]:text-neutral-950" item_indicator_class="col-start-1" item_text_class="col-start-2" > <:icon> <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" class="block"> <path d="M11 10H5l3 3.5zm0-4H5l3-3.5z" /> </svg> </:icon> <:item_indicator> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" class="block" > <path d="m2.5 8.5 4 4 7-9" /> </svg> </:item_indicator> <:option value="javascript">JavaScript</:option> <:option value="typescript">TypeScript</:option> <:option value="python">Python</:option> <:option value="java">Java</:option> <:option value="csharp">C#</:option> <:option value="php">PHP</:option> <:option value="cpp">C++</:option> <:option value="rust">Rust</:option> <:option value="go">Go</:option> <:option value="swift">Swift</:option> </.select>
Base UI Select example - object values (Phoenix headless)
- Standard Delivers in 4-6 business days ($4.99)
- Express Delivers in 2-3 business days ($9.99)
- Overnight Delivers next business day ($19.99)
<.select id="baseui-select-object-values" label="Shipping method" value="standard" class="flex flex-col items-start gap-1" label_class="cursor-default text-sm font-bold text-neutral-950 dark:text-white" trigger_class="flex min-h-8 min-w-[16rem] items-center justify-between gap-3 pl-2 pr-1 py-1.5 text-sm leading-none whitespace-nowrap border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 text-neutral-950 dark:text-white select-none hover:not-data-[disabled]:bg-neutral-100 dark:hover:not-data-[disabled]:bg-neutral-800 active:not-data-[disabled]:bg-neutral-200 dark:active:not-data-[disabled]:bg-neutral-700 data-[disabled]:border-neutral-500 data-[disabled]:text-neutral-500 disabled:border-neutral-500 disabled:text-neutral-500 dark:data-[disabled]:border-neutral-400 dark:data-[disabled]:text-neutral-400 data-[popup-open]:bg-neutral-100 dark:data-[popup-open]:bg-neutral-800 font-normal focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:focus-visible:outline-white" icon_class="flex items-center" positioner_class="outline-hidden select-none z-10" popup_class="group min-w-[var(--anchor-width)] origin-[var(--transform-origin)] py-1 bg-clip-padding border border-neutral-950 bg-white text-neutral-950 outline-hidden shadow-[0.25rem_0.25rem_0] shadow-black/12 transition-[scale,opacity] duration-100 ease-out data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[side=none]:translate-y-px data-[side=none]:min-w-[calc(var(--anchor-width)+1.75rem)] data-[side=none]:data-[ending-style]:transition-none data-[starting-style]:scale-[0.98] data-[starting-style]:opacity-0 data-[side=none]:data-[starting-style]:scale-100 data-[side=none]:data-[starting-style]:opacity-100 data-[side=none]:data-[starting-style]:transition-none dark:border-white dark:bg-neutral-950 dark:text-white dark:shadow-none" item_class="group/item grid cursor-default grid-cols-[1rem_1fr] items-start gap-2 py-1.5 pr-4 pl-2.5 text-sm outline-hidden select-none data-[highlighted]:bg-neutral-950 data-[highlighted]:text-white dark:data-[highlighted]:bg-white dark:data-[highlighted]:text-neutral-950" item_indicator_class="col-start-1 flex items-center justify-center self-start relative top-[0.4em]" item_text_class="col-start-2 flex flex-col gap-0.5" > <:icon> <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" class="block"> <path d="M11 10H5l3 3.5zm0-4H5l3-3.5z" /> </svg> </:icon> <:item_indicator> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" class="block" > <path d="m2.5 8.5 4 4 7-9" /> </svg> </:item_indicator> <:option value="standard"> <span class="text-sm">Standard</span> <span class="text-xs text-neutral-600 group-data-[highlighted]/item:text-neutral-400 dark:text-neutral-400 dark:group-data-[highlighted]/item:text-neutral-600"> Delivers in 4-6 business days ($4.99) </span> </:option> <:option value="express"> <span class="text-sm">Express</span> <span class="text-xs text-neutral-600 group-data-[highlighted]/item:text-neutral-400 dark:text-neutral-400 dark:group-data-[highlighted]/item:text-neutral-600"> Delivers in 2-3 business days ($9.99) </span> </:option> <:option value="overnight"> <span class="text-sm">Overnight</span> <span class="text-xs text-neutral-600 group-data-[highlighted]/item:text-neutral-400 dark:text-neutral-400 dark:group-data-[highlighted]/item:text-neutral-600"> Delivers next business day ($19.99) </span> </:option> </.select>
Parts & data attributes
Each part carries a stable
data-part
and a structural
chelekom-select__*
class you can target, plus paired-presence state for open/closed transitions.
| Part | data-part | State / ARIA | Purpose |
|---|---|---|---|
| Label | label | - | Accessible name, wired via aria-labelledby. |
| Trigger | trigger | role="combobox", aria-haspopup, aria-expanded, aria-controls, aria-readonly, aria-required, data-popup-open, data-placeholder, data-disabled, data-readonly, data-required | The button that opens the listbox. |
| Value | value | data-placeholder | Selected label(s), or the placeholder when empty. |
| Icon | icon | data-popup-open | The caret glyph inside the trigger. |
| Popup | popup | role="listbox", aria-multiselectable, data-open, data-closed, data-side | The floating option list. |
| Group / Group label | group / group-label | role="group", aria-labelledby | Consecutive same-group options wrapped with a muted label. |
| Item / Item indicator | item / item-indicator | role="option", aria-selected, data-selected, data-highlighted, data-disabled | A single option and its selected-check indicator. |
Accessibility
Follows the WAI-ARIA Combobox pattern : Enter, Space or an Arrow key opens the popup; inside it, Arrow Up/Down and Home/End move a roving highlight, Enter/Space commits the highlighted option, Escape/Tab closes it, and typing jumps to a matching option (typeahead). The selection mirrors into hidden inputs so it participates in normal form submission.