Select

Dropdown for choosing one or many values, with optional search, grouped options, multi-select with caps, and a native variant.

Basic

.heex
<div class="w-[300px]">
  <.select
    name="basic_country"
    placeholder="Select a country"
    options={[
      {"United States", "US"},
      {"Canada", "CA"},
      {"United Kingdom", "UK"},
      {"Germany", "DE"},
      {"France", "FR"},
      {"Japan", "JP"}
    ]}
  />
</div>

Label, description, and help text

Combine label, sublabel, description, and help_text to render the full form-field chrome around the toggle.

Required

Sets default tax and shipping rates.

You can update this later from your profile.
.heex
<.select
  name="country"
  label="Country of residence"
  sublabel="Required"
  description="Sets default tax and shipping rates."
  help_text="You can update this later from your profile."
  placeholder="Select a country..."
  options={@countries}
/>

Sizes

.heex
<.select size="xs" name="genre_xs" label="Genre (xs)" options={@genres} />
<.select size="sm" name="genre_sm" label="Genre (sm)" options={@genres} />
<.select size="md" name="genre_md" label="Genre (md)" options={@genres} />
<.select size="lg" name="genre_lg" label="Genre (lg)" options={@genres} />
<.select size="xl" name="genre_xl" label="Genre (xl)" options={@genres} />

Pre-selected value

Pass value to set the initial selection. The toggle renders the matching option's label on first paint.

.heex
<.select
  name="rating"
  label="Movie rating"
  value="pg13"
  options={[
    {"G - General Audiences", "g"},
    {"PG - Parental Guidance", "pg"},
    {"PG-13 - Parents Strongly Cautioned", "pg13"},
    {"R - Restricted", "r"},
    {"NC-17 - Adults Only", "nc17"}
  ]}
/>

Disabled options

Mark individual options with disabled: true. They are visually muted, ignored by clicks, and skipped during keyboard navigation and typeahead.

.heex
<div class="w-[300px]">
  <.select
    name="framework"
    label="Frontend framework"
    placeholder="Select a framework..."
    options={[
      [key: "React", value: "react"],
      [key: "Vue", value: "vue"],
      [key: "Angular", value: "angular", disabled: true],
      [key: "Svelte", value: "svelte"],
      [key: "Ember", value: "ember", disabled: true],
      [key: "Solid", value: "solid"]
    ]}
  />
</div>

Option formats

Options accept any shape Phoenix.HTML.Form.options_for_select/2 understands: bare strings, {label, value} tuples, atom-keyed keyword pairs, ranges, and more.

.heex
<.select name="strings" label="Bare strings" options={["Love", "Revenge", "Justice"]} />

<.select
  name="keyword_pairs"
  label="Keyword pairs"
  options={[english: "en", spanish: "es", french: "fr"]}
/>

<.select name="numeric_range" label="Numeric range" options={1..12} />

Grouped and nested options

Wrap options in {group_label, children} tuples. Groups can nest to any depth, and each level receives a --depth CSS variable for indentation.

.heex
<.select
  name="city"
  label="Office location"
  placeholder="Select a city..."
  options={[
    {"Americas",
     [
       {"United States", [{"New York", "nyc"}, {"San Francisco", "sf"}]},
       {"Canada", [{"Toronto", "toronto"}, {"Vancouver", "vancouver"}]}
     ]},
    {"Europe",
     [
       {"France", [{"Paris", "paris"}, {"Lyon", "lyon"}]},
       {"Germany", [{"Berlin", "berlin"}, {"Munich", "munich"}]}
     ]}
  ]}
/>

Native mode

Pass native to render a plain HTML select. Drops search, multi-select, custom rendering, and affix slots, but keeps labels, sizing, and form-field binding.

.heex
<.select
  native
  name="country"
  label="Country"
  placeholder="Select a country..."
  options={[{"United States", "US"}, {"Canada", "CA"}]}
/>

Searchable

Pass searchable to add a sticky search input on top of the listbox. Filtering is case-insensitive, the query persists across close/reopen, and arrow keys forward to the listbox.

.heex
<.select
  name="country"
  label="Country"
  searchable
  search_input_placeholder="Search countries..."
  options={@countries}
/>

Search threshold

Set search_threshold to require a minimum query length before filtering kicks in. Below the threshold, the full list is shown and no on_search event is dispatched.

.heex
<.select
  name="actor"
  label="Actor"
  searchable
  search_threshold={2}
  search_input_placeholder="Type at least 2 characters..."
  options={@actors}
/>

Listbox height

The listbox is capped at max-h-72 and shrinks to fit the space available, only opening on the other side once that space gets too small. Pass another max-h-* through class to change the cap, or max-h-none to use all the space on whichever side it opens.

.heex
<.select name="timezone" options={@timezones} />
<.select name="timezone" options={@timezones} class="max-h-96" />
<.select name="timezone" options={@timezones} class="max-h-none" />

Pass on_search to delegate filtering to a LiveView handler. The component debounces input, shows a loading indicator, and patches the list in place while preserving open and highlight state.

Cascading selects

Drive each select's options from the previous one's value. A single phx-change fires for the whole form; the LiveView swaps the dependent options based on the new value and the listbox patches in place while preserving open state. Disable each dependent field until its parent has a value, and reset its selection if the parent changes to a value where the previous choice no longer applies.

.heex
<.form :let={f} for={@form} phx-change="change">
  <.select
    field={f[:country]}
    label="Country"
    options={@countries}
    placeholder="Select a country..."
    clearable
  />

  <.select
    field={f[:state]}
    label="State"
    options={@states}
    placeholder="Pick a country first"
    disabled={@states == []}
    clearable
  />

  <.select
    field={f[:city]}
    label="City"
    options={@cities}
    placeholder="Pick a state first"
    disabled={@cities == []}
    clearable
  />
</.form>
.ex
def handle_event("change", _params, socket) do
  # Recompute :states and :cities from the new country/state, and
  # clear any selection no longer covered by the parent value
  {:noreply, socket}
end

Multiple selection

Pass multiple to accept several values. The dropdown stays open after each click for picking a few in a row, and form params arrive as a list.

.heex
<.select
  name="genres[]"
  label="Genres"
  multiple
  placeholder="Pick a few..."
  options={@genres}
/>

Capped multi-select

Combine multiple with max to cap how many values can be picked. Once the cap is reached, additional selections are ignored until something is deselected.

.heex
<.select
  name="favorites[]"
  label="Top 3 genres"
  multiple
  max={3}
  placeholder="Pick up to 3..."
  options={@genres}
/>

Clearable

Pass clearable to expose a clear button in the toggle. Clicking the active option also deselects it, and pressing Backspace while the toggle is focused clears the selection.

.heex
<.select
  name="country"
  label="Country"
  clearable
  value="US"
  options={@countries}
/>

Multi-select with chip toggle labels

Combine multiple with a :toggle_label slot to render each selection as a colored chip instead of a comma-separated list. The slot is invoked once per selected option and runs inside the default toggle chrome.

.heex
<.select
  name="tags[]"
  label="Issue tags"
  multiple
  searchable
  clearable
  value={["bug", "p1", "backend"]}
  placeholder="Add tags..."
  options={@tags}
>
  <:toggle_label :let={{label, value}}>
    <.badge variant="soft" color={tag_color(value)} size="xs">{label}</.badge>
  </:toggle_label>
</.select>

Inner affixes

:inner_prefix sits inside the toggle border, before the value area. :inner_suffix replaces the default chevron, so render your own indicator if it is still needed.

.heex
<.select name="service" placeholder="Pick a service..." options={@services}>
  <:inner_prefix>
    <.icon name="hero-magnifying-glass" class="icon" />
  </:inner_prefix>
</.select>

<.select name="service" placeholder="Pick a service..." options={@services}>
  <:inner_suffix>
    <.icon name="hero-tv" class="icon" />
  </:inner_suffix>
</.select>

Outer affixes (input group)

:outer_prefix and :outer_suffix sit beside the field, sharing a border to form a single input group. Match the affix child's size to the select's.

Filter
.heex
<.select name="filter" placeholder="All categories" options={@categories}>
  <:outer_prefix class="px-3 font-medium text-foreground bg-sunken/40">
    Filter
  </:outer_prefix>
</.select>

<.select name="lookup" placeholder="Choose option..." options={@items}>
  <:outer_suffix>
    <.button>
      <.icon name="hero-arrow-right" class="icon" /> Apply
    </.button>
  </:outer_suffix>
</.select>

:header and :footer stay pinned in the listbox while the options scroll. Drop filter chips above the list, or a 'create new' action below it.

:footer can host a LiveView-bound action like 'Create new'. The handler appends to the options assign and the listbox patches in place, so the dropdown stays open and context is preserved.

Create new via modal

Combine the :footer slot with JS helpers to drive a full create flow. The footer button chains Fluxon.close_select/1 with Fluxon.open_dialog/1 client-side, so the dropdown collapses as the modal animates in. On submit, the handler appends the new option, pre-selects it by setting the form value, and chains Fluxon.close_dialog/2 with Fluxon.open_select/2 on the socket. The listbox patches in place and reopens with the new tag already selected.

.heex
<.form :let={f} for={@form} phx-change="change">
  <.select
    id="tag-select"
    field={f[:tag]}
    label="Tag"
    options={@tags}
    placeholder="Pick a tag..."
    searchable
  >
    <:footer class="p-1 border-t border-base">
      <.button
        type="button"
        size="sm"
        variant="ghost"
        class="w-full"
        phx-click={Fluxon.close_select("tag-select") |> Fluxon.open_dialog("tag-modal")}
      >
        <.icon name="huge-add-01" class="icon" /> Create new tag
      </.button>
    </:footer>
  </.select>
</.form>

<.modal
  id="tag-modal"
  class="w-[400px]"
  on_close={JS.focus(to: "#tag-select-toggle")}
>
  <h2 class="text-lg font-semibold">Create new tag</h2>

  <.form :let={f} for={@new_tag_form} as={:new_tag} phx-submit="create_tag" class="mt-4 space-y-4">
    <.input field={f[:name]} label="Tag name" placeholder="e.g. Performance" autofocus />

    <div class="flex justify-end gap-2">
      <.button type="button" variant="ghost" phx-click={Fluxon.close_dialog("tag-modal")}>
        Cancel
      </.button>
      <.button type="submit" variant="solid">Create tag</.button>
    </div>
  </.form>
</.modal>
.ex
def handle_event("create_tag", %{"new_tag" => %{"name" => name}}, socket) do
  value = slugify(name)

  {:noreply,
   socket
   |> assign(
     tags: socket.assigns.tags ++ [{name, value}],
     # Pre-select the new tag by setting the form value.
     form: to_form(%{"tag" => value}),
     new_tag_form: to_form(%{"name" => ""})
   )
   # Dismiss the modal and re-open the select so the user
   # sees the newly created tag selected.
   |> Fluxon.close_dialog("tag-modal")
   |> Fluxon.open_select("tag-select")}
end

Custom option rendering

Pass an :option slot to fully control each row. The {label, value} tuple is yielded to the slot, and the wrapping element exposes data-highlighted and data-selected for styling.

.heex
<.select
  field={f[:role]}
  placeholder="Role"
  options={Enum.map(@roles, fn role -> {role.value, role.name} end)}
>
  <:option :let={{value, label}}>
    <div class="cursor-default px-3 py-2 rounded-lg in-data-highlighted:bg-blue-500 [[data-highlighted]_&]:flx-focus:bg-blue-500">
      <div class="font-medium text-sm/6 in-data-highlighted:text-white"><%= label %></div>
      <div class="text-zinc-500 text-xs in-data-highlighted:text-zinc-100">
        <%= Enum.find(@roles, fn r -> r.value == value end).description %>
      </div>
    </div>
  </:option>
</.select>


<.select
  field={f[:country]}
  options={Enum.map(@countries, fn country -> {country.iso, country.name} end)}
>
  <:option :let={{value, label}}>
    <div class="cursor-default in-data-highlighted:bg-zinc-100 px-2 py-1 rounded-sm flx-selected:bg-blue-500 [[data-highlighted]_&]:flx-selected:bg-blue-500">
      <.icon name={icon_name(value)} class="rounded-xs mr-1 shadow-sm w-4" />
      <span class="flx-selected:text-white"><%= label %></span>
    </div>
  </:option>
</.select>

User picker (option and toggle label together)

Pair :option and :toggle_label to render rich rows in the dropdown and a compact chip in the toggle. The :option slot drives the listbox; :toggle_label drives the selected-value display while keeping the default chrome (border, focus ring, chevron, clear button).

.heex
<.select
  field={f[:assignee]}
  label="Assignee"
  options={Enum.map(@users, &{&1.name, &1.value})}
  placeholder="Assign teammate..."
  searchable
  clearable
>
  <:toggle_label :let={{label, value}}>
    <% user = Enum.find(@users, &(&1.value == value)) %>
    <span class="flex items-center gap-2 min-w-0">
      <span class={["size-5 shrink-0 rounded-full text-[10px] text-white flex items-center justify-center", user.color]}>
        {user.initials}
      </span>
      <span class="truncate">{label}</span>
    </span>
  </:toggle_label>

  <:option :let={{label, value}}>
    <% user = Enum.find(@users, &(&1.value == value)) %>
    <div class="flex items-center gap-3 px-2 py-2 rounded-base in-data-highlighted:highlight in-data-selected:font-medium">
      <span class={["size-9 shrink-0 rounded-full text-xs font-semibold text-white flex items-center justify-center", user.color]}>
        {user.initials}
      </span>
      <div class="min-w-0 flex-1">
        <div class="text-sm truncate">{label}</div>
        <div class="text-xs text-foreground-softer truncate font-normal">{user.role}</div>
      </div>
      <.icon name="huge-tick-02" class="hidden in-data-selected:block size-4" />
    </div>
  </:option>
</.select>

Custom toggle label

Use :toggle_label to customize how the selected value is rendered while keeping the default chrome (border, focus ring, chevron, clear button, affixes).

.heex
<.select name="status" label="Issue status" value="in_review" options={@statuses}>
  <:toggle_label :let={{label, value}}>
    <span class="flex items-center gap-2">
      <span class={[
        "size-2 rounded-full",
        value == "open" && "bg-blue-500",
        value == "in_review" && "bg-amber-500",
        value == "approved" && "bg-emerald-500",
        value == "closed" && "bg-zinc-400"
      ]} />
      {label}
    </span>
  </:toggle_label>
</.select>

Custom toggle (full replacement)

Use :toggle to replace the entire trigger element. The component still wires up open/close, keyboard, and ARIA, but the visual design (border, padding, hover, focus, indicator) is yours. The slot is invoked once per selected option, so set an initial value or render the field only after a selection exists.

Grace Hopper
.heex
<.select name="assignee" value="grace" options={@users}>
  <:toggle :let={{label, _value}}>
    <div class="flex items-center gap-2 px-3 py-2 border border-base rounded-base bg-input cursor-default hover:border-foreground-softer">
      <.icon name="hero-user-circle" class="size-5 text-foreground-softer" />
      <span class="text-sm flex-1 truncate">{label}</span>
      <.icon name="hero-chevron-up-down" class="size-4 text-foreground-softest" />
    </div>
  </:toggle>
</.select>

Form integration

Bind to a Phoenix form field with field={f[:...]}. Name, value, id, and validation errors are derived from the form, and a hidden native select keeps standard form submissions and validations working.

.heex
<.form :let={f} for={@form} phx-change="validate" phx-submit="save">
  <.select
    field={f[:genre]}
    label="Movie Genre"
    options={@genres}
    placeholder="Select genre..."
    clearable
    searchable
  />

  <.select
    field={f[:rating]}
    label="Movie Rating"
    options={@ratings}
    placeholder="Choose rating..."
    clearable
  />

  <.button type="submit">Save Movie</.button>
</.form>
.ex
def handle_event("validate", %{"movie_form" => _params}, socket) do
  # Re-run validation and reassign the form
  {:noreply, socket}
end

def handle_event("save", %{"movie_form" => _params}, socket) do
  # Persist the movie
  {:noreply, socket}
end

Validation errors

Pass errors as a list of strings to render messages and apply invalid styling. Bound forms populate this automatically once the input has been touched.

.heex
<.select
  name="rating"
  label="Movie rating"
  errors={["This field is required"]}
  options={@ratings}
/>

<.select
  name="advisory"
  label="Content advisory"
  errors={["This field is required", "Pick at least one advisory"]}
  options={@advisories}
/>

Disabled

.heex
<.select
  name="country"
  label="Country"
  disabled
  value="US"
  options={[{"United States", "US"}, {"Canada", "CA"}]}
/>