Select
v0.0.10
mix mishka.ui.gen.headless select
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.
In a grouped list, group_class styles each group, group_label_class its label and group_list_class the list of options inside it, so group_list_class="pl-4" indents the options under their label. An option slot adds its own class to one item and text_class to its text.
Base UI Select example - single select (Phoenix headless)
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)
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)
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)
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-snug text-start max-w-full 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. Takes label_class. |
| 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. Takes trigger_class. |
| Value | value | data-placeholder | Selected label(s), or the placeholder when empty. Takes value_class. |
| Icon | icon | data-popup-open | The caret glyph inside the trigger. Takes icon_class. |
| Positioner | positioner | - | The wrapper the popup is placed with. Takes positioner_class. |
| Popup | popup | role="listbox", aria-multiselectable, data-open, data-closed, data-side | The floating option list. Takes popup_class. |
| Group / Group label | group / group-label | role="group", aria-labelledby | Consecutive same-group options wrapped with a muted label. Take group_class and group_label_class. |
| Group list | (no data-part) | role="presentation" | The <ul> that holds a group's options. Takes group_list_class. |
| Item / Item indicator | item / item-indicator | role="option", aria-selected, data-selected, data-highlighted, data-disabled | A single option and its selected-check indicator. Take item_class (or class on an option slot) and item_indicator_class. |
| Item text | item-text | - | The option's label inside the item. Takes item_text_class on the root and text_class on an option slot. |
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.
Attributes
| Name | Type | Default | Values | Required | Description |
|---|---|---|---|---|---|
id
|
string
| — | — | Yes | — |
name
|
string
| — | — | No | Name for the hidden form input(s) |
value
|
any
| — | — | No | Selected value (string), or a list of values when multiple |
placeholder
|
string
|
Select…
| — | No | Shown (with data-placeholder) when nothing is selected |
label
|
string
| — | — | No | Accessible label (a <label> wired via aria-labelledby) |
multiple
|
boolean
|
false
| — | No | Allow selecting several options (stays open; submits name[]) |
disabled
|
boolean
|
false
| — | No | Disable the select (data-disabled) |
readonly
|
boolean
|
false
| — | No | Block changing the selection (data-readonly) |
required
|
boolean
|
false
| — | No | Require a selection for form submit (data-required) |
form
|
string
| — | — | No | Form id owning the hidden input(s) |
side
|
string
|
bottom
| — | No | Popup side: bottom | top | left | right |
highlight_on_hover
|
boolean
|
true
| — | No | Highlight items on pointer hover |
on_change
|
string
| — | — | No | LiveView event pushed on selection ({value}) |
on_open_change
|
string
| — | — | No | LiveView event pushed on open/close ({open}) |
class
|
any
| — | — | No | — |
label_class
|
any
| — | — | No | Extra classes for the label part |
trigger_class
|
any
| — | — | No | Extra classes for the trigger part |
value_class
|
any
| — | — | No | Extra classes for the value part |
icon_class
|
any
| — | — | No | Extra classes for the trigger icon part |
positioner_class
|
any
| — | — | No | Extra classes for the positioner part |
popup_class
|
any
| — | — | No | Extra classes for the popup part |
group_class
|
any
| — | — | No | Extra classes for each group part |
group_label_class
|
any
| — | — | No | Extra classes for each group-label part |
item_class
|
any
| — | — | No | Extra classes for each item part |
item_indicator_class
|
any
| — | — | No | Extra classes for each item-indicator part |
item_text_class
|
any
| — | — | No | Extra classes for each item-text part |
group_list_class
|
any
| — | — | No | — |
rest
|
global
| — | — | No | — |
Slots
| Name | Required | Description |
|---|---|---|
icon
| No | Custom trigger icon (replaces the default caret glyph) |
item_indicator
| No | Custom selected-item indicator (replaces the default check glyph) |
option
| Yes | — |
Attributes of the option slot
| Name | Type | Default | Values | Required | Description |
|---|---|---|---|---|---|
value
|
string
| — | — | Yes | — |
disabled
|
boolean
| — | — | No | Disable just this option |
group
|
string
| — | — | No | Optional group label (consecutive same-group options are grouped) |
class
|
any
| — | — | No | Extra classes for this item part |
text_class
|
any
| — | — | No | Extra classes for this item-text part |
Options
Each option takes one or more of its values, separated by commas.
| Option | Values |
|---|---|
--type
|
select
|
--module
| A module name of your own |
What it generates
mix mishka.ui.gen.headless select