Select

v0.0.10

v0.0.10StableHeadlessStatelessAccessibleJSApache-2.0
Installation mix mishka.ui.gen.headless select
Source View Source

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
heex
<.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
  • Vegetables
heex
<.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
heex
<.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)
heex
<.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.

Partdata-partState / ARIAPurpose
Labellabel-Accessible name, wired via aria-labelledby. Takes label_class.
Triggertriggerrole="combobox", aria-haspopup, aria-expanded, aria-controls, aria-readonly, aria-required, data-popup-open, data-placeholder, data-disabled, data-readonly, data-requiredThe button that opens the listbox. Takes trigger_class.
Valuevaluedata-placeholderSelected label(s), or the placeholder when empty. Takes value_class.
Iconicondata-popup-openThe caret glyph inside the trigger. Takes icon_class.
Positionerpositioner-The wrapper the popup is placed with. Takes positioner_class.
Popuppopuprole="listbox", aria-multiselectable, data-open, data-closed, data-sideThe floating option list. Takes popup_class.
Group / Group labelgroup / group-labelrole="group", aria-labelledbyConsecutive 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 indicatoritem / item-indicatorrole="option", aria-selected, data-selected, data-highlighted, data-disabledA single option and its selected-check indicator. Take item_class (or class on an option slot) and item_indicator_class.
Item textitem-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.