Fluxon.Components.Select (Fluxon v2.3.4)

A modern, accessible select component for building rich selection interfaces in Phoenix LiveView.

The select component provides single and multiple selection, client-side and server-side search, custom option rendering, grouped and nested options, and full keyboard navigation. It operates in two modes: a feature-rich custom select (default) backed by a hidden <select> element for form compatibility, and a lightweight native mode that renders the browser's built-in <select> control.

Select vs Autocomplete

Both components let users choose from a list of options, but they target different interaction patterns. Use this table to decide which fits your use case.

FeatureSelectFluxon.Components.Autocomplete
Primary interactionClick to browseType to search
Best forSmall to medium listsAny size dataset
FilteringClient-side (+ optional server)Client or server-side
Multiple selectionYesNo
ClearableYesYes
Native modeYesNo

Usage

Basic usage with a list of options:

<.select
  name="country"
  options={[{"United States", "us"}, {"Canada", "ca"}]}
/>

The component follows the same options API as Phoenix.HTML.Form.options_for_select/2, supporting multiple formats:

# List of strings
<.select name="fruit" options={["Apple", "Banana", "Cherry"]} />

# List of tuples (label, value)
<.select name="country" options={[{"United States", "us"}, {"Canada", "ca"}]} />

# Keyword list
<.select name="lang" options={[English: "en", Spanish: "es", French: "fr"]} />

# Keyword list with key/value/disabled
<.select
  name="role"
  options={[
    [key: "Admin", value: "admin"],
    [key: "User", value: "user", disabled: true],
    [key: "Guest", value: "guest"]
  ]}
/>

# Grouped options
<.select
  name="city"
  options={[
    {"North America", [{"New York", "nyc"}, {"Toronto", "tor"}]},
    {"Europe", [{"London", "lon"}, {"Paris", "par"}]}
  ]}
/>

# Multi-level nested groups (flattened with visual indentation)
<.select
  name="city"
  options={[
    {"Europe", [
      {"France", [{"Paris", 1}, {"Lyon", 2}]},
      {"Italy", [{"Rome", 3}, {"Milan", 4}]}
    ]},
    {"Asia", [
      {"China", [{"Beijing", 5}, {"Shanghai", 6}]}
    ]}
  ]}
/>

A full-featured example combining labels, description, help text, and placeholder:

<.select
  name="payment_method"
  label="Payment Method"
  sublabel="Required"
  description="Choose your preferred payment method"
  help_text="We securely process all payment information"
  placeholder="Select payment method"
  options={[
    {"Credit Card", "credit_card"},
    {"PayPal", "paypal"},
    {"Bank Transfer", "bank_transfer"},
    {"Cryptocurrency", "crypto"}
  ]}
/>

Native Select

Set native to render a standard HTML <select> element instead of the custom interface. This is ideal for mobile-first designs, simple forms, or when maximum browser compatibility is required.

<.select
  name="country"
  native
  placeholder="Pick a country"
  options={[{"United States", "us"}, {"Canada", "ca"}]}
/>

Native mode limitations

Features such as search, multiple selection, clearable, custom option rendering, header/footer slots, and affix slots are not available in native mode. Use the custom select for those capabilities.

Size Variants

The select component offers five size variants to fit different interface densities:

<.select name="xs" size="xs" options={@options} placeholder="Extra small" />
<.select name="sm" size="sm" options={@options} placeholder="Small" />
<.select name="md" size="md" options={@options} placeholder="Medium (default)" />
<.select name="lg" size="lg" options={@options} placeholder="Large" />
<.select name="xl" size="xl" options={@options} placeholder="Extra large" />
SizeHeightUse Case
"xs"h-7Compact UIs, dense layouts, inline controls
"sm"h-8Secondary inputs, sidebar filters
"md"h-9Default -- recommended for most forms
"lg"h-10Prominent inputs, settings pages
"xl"h-11Hero sections, onboarding flows

Each size variant adjusts height, padding, font size, icon dimensions, and affix sizing proportionally.

Labels and Descriptions

Provide context and guidance with comprehensive labeling:

# Basic label
<.select name="country" label="Country" options={@countries} />

# Label with sublabel
<.select name="country" label="Country" sublabel="Required" options={@countries} />

# Label with description
<.select
  name="country"
  label="Country"
  description="Where is your company headquartered?"
  options={@countries}
/>

# Complete example with help text
<.select
  name="country"
  label="Country"
  sublabel="Optional"
  description="Used for tax calculation purposes"
  help_text="You can change this later in settings"
  placeholder="Select a country..."
  options={@countries}
/>

Form Integration

The select component integrates with Phoenix forms in two ways: using the field attribute for full form integration or using the name attribute for standalone selects.

Use the field attribute to bind the select to a form field:

<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save">
  <.select
    field={f[:country]}
    label="Country"
    options={@countries}
  />
</.form>

Using the field attribute provides:

  • Automatic value handling from form data
  • Error handling and validation messages
  • Form submission with correct field names
  • Integration with changesets
  • ID generation for accessibility
  • Nested form data handling

Complete example with a changeset implementation:

defmodule MyApp.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :country, :string
    field :languages, {:array, :string}
    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:country, :languages])
    |> validate_required([:country])
    |> validate_subset(:languages, ["en", "es", "fr", "de"])
  end
end

# In your LiveView
def mount(_params, _session, socket) do
  countries = [{"United States", "us"}, {"Canada", "ca"}, {"Mexico", "mx"}]
  languages = [{"English", "en"}, {"Spanish", "es"}, {"French", "fr"}, {"German", "de"}]

  changeset = User.changeset(%User{}, %{})

  {:ok, assign(socket, countries: countries, languages: languages, form: to_form(changeset))}
end

def render(assigns) do
  ~H"""
  <.form :let={f} for={@form} phx-change="validate">
    <.select clearable field={f[:country]} options={@countries} label="Country" />
    <.select clearable multiple field={f[:languages]} options={@languages} label="Languages" />
  </.form>
  """
end

def handle_event("validate", %{"user" => params}, socket) do
  changeset =
    %User{}
    |> User.changeset(params)
    |> Map.put(:action, :validate)

  {:noreply, assign(socket, form: to_form(changeset))}
end

Using Standalone Selects

For simpler cases or when not using Phoenix forms, use the name attribute:

<.select
  name="sort_by"
  value="newest"
  options={[
    {"Newest First", "newest"},
    {"Oldest First", "oldest"},
    {"Name A-Z", "name_asc"}
  ]}
/>

When using standalone selects:

  • The name attribute determines the form field name
  • Values are managed through the value attribute
  • Errors are passed via the errors attribute

Input States

# Disabled state
<.select name="locked" disabled options={@options} value="locked_value" />

# Error state (standalone)
<.select name="country" options={@countries} errors={["is required"]} />

# Error state (form integration -- errors are handled automatically)
<.select field={f[:country]} options={@countries} />

Searchable

Enable the built-in search input to filter options by setting searchable:

<.select
  name="country"
  searchable
  search_input_placeholder="Search for a country"
  search_no_results_text="No countries found for %{query}"
  options={[
    {"United States", "us"},
    {"Canada", "ca"},
    {"Mexico", "mx"}
  ]}
/>

Search Behavior

Client-side search provides immediate feedback with these characteristics:

  • Case-insensitive matching
  • Diacritics-insensitive (accents are ignored via Unicode normalization)
  • Filters update as you type
  • Full keyboard navigation remains active during search
  • Search input receives focus automatically when the select opens

The search matches against option labels. For example, searching for "united" will match "United States" regardless of case or accents.

Custom Search Messages

Two attributes control the search text:

  • search_input_placeholder: Text shown in the search input (default: "Search...")
  • search_no_results_text: Text shown when no options match (default: "No results found for %{query}.")

The no results message can include the search term with %{query}:

<.select
  name="country"
  searchable
  search_input_placeholder="Find a country..."
  search_no_results_text="No countries matching '%{query}'"
  options={@countries}
/>

Search with Multiple Selection

When combining search and multiple selection, the component preserves the selected state of filtered-out options. Selected labels remain visible in the toggle button even when those options are hidden by the search filter:

<.select
  name="countries"
  multiple
  searchable
  options={@countries}
  placeholder="Select countries"
  search_input_placeholder="Search countries..."
/>

Search with Custom Options

Search matches against option labels while preserving your custom rendering:

<.select name="user" searchable options={@users}>
  <:option :let={{label, value}}>
    <div class="flex items-center gap-2 px-3 py-2">
      <img src={avatar_url(value)} class="size-8 rounded-full" />
      <div>
        <div class="font-medium">{label}</div>
        <div class="text-sm text-zinc-500">{user_role(value)}</div>
      </div>
    </div>
  </:option>
</.select>

Search Implementation Details

The client-side search:

  • Runs entirely in JavaScript for immediate feedback
  • Normalizes text to lowercase with NFD Unicode decomposition (strips diacritics)
  • Matches against the full option label
  • Updates the filtered list without changing the selected values
  • Maintains all keyboard navigation features during search

For large datasets or dynamically computed results, enable server-side search by providing the on_search attribute with a LiveView event name:

<.select
  field={f[:user_id]}
  searchable
  search_threshold={2}
  debounce={500}
  on_search="search_users"
  search_input_placeholder="Search users..."
  search_no_results_text="No users found for '%{query}'"
  options={@filtered_users}
/>

When server-side search is enabled:

  • The on_search event is triggered after the debounce period
  • The event receives %{"query" => query, "id" => component_id} as parameters
  • A loading spinner is displayed while the search is in progress
  • The component reinitializes from the DOM once LiveView patches new options

Important: Selected Options and Search Results

This is a fundamental constraint of server-side search that requires careful consideration.

The toggle label is derived from the currently available options in the options attribute. If a user has selected an option and then performs a search that filters it out, the toggle label will become empty until the option reappears in the results.

Recommended implementation: Always include currently selected options in your server search results, even when they do not match the query. This prevents the toggle label from disappearing.

Search Configuration

AttributeDefaultDescription
search_threshold0Minimum characters required before triggering a search
debounce300Milliseconds to wait after typing stops before searching
on_searchnilLiveView event name; when set, enables server-side search

Example with all search options configured:

<.select
  field={f[:product_id]}
  searchable
  search_threshold={3}
  debounce={400}
  on_search="search_products"
  search_input_placeholder="Type at least 3 characters to search..."
  search_no_results_text="No products found matching '%{query}'"
  options={@products}
/>

Multiple Selection

Enable multiple selection with the multiple attribute:

<.select
  name="countries"
  multiple
  placeholder="Select countries"
  options={[{"United States", "us"}, {"Canada", "ca"}, {"Mexico", "mx"}]}
/>

This allows selecting multiple options and submits an array of values:

%{"_target" => ["countries"], "countries" => ["", "us", "ca", "mx"]}

Empty values in multiple select

Per the HTML specification, a <select multiple> with no selected options omits the field from form data entirely. To ensure the field is always present, the component injects a hidden input with an empty value:

<input type="hidden" name="countries[]" value="" />
<select multiple name="countries[]">
  <!-- options here -->
</select>

This results in consistent form submissions:

# No options selected
%{"countries" => [""]}

# One or more selected
%{"countries" => ["", "us", "ca"]}

When processing form data, filter out the empty string:

selected = Enum.reject(params["countries"] || [], &(&1 == ""))

By default, there is no limit on the number of selections.

Maximum Selections

Use the max attribute to cap the number of selections:

<.select name="tags" multiple max={3} options={@tags} placeholder="Select up to 3 tags" />

When the maximum is reached, additional options become unselectable until an existing selection is removed.

Clearable

By default, single selections cannot be unselected. The clearable attribute adds a clear button and enables re-clicking a selected option to deselect it. Pressing Backspace when the toggle is focused also clears the selection. For multiple selections, the clear button removes all selections at once.

<.select name="country" clearable options={@countries} placeholder="Select a country" />

When clearing a single selection, the empty placeholder option (<option value="">) is selected, sending an empty string in the form data -- consistent with how multiple select handles empty states.

Disabled Options

Individual options can be disabled using the keyword list format with a disabled key:

<.select
  name="role"
  options={[
    [key: "Admin", value: "admin"],
    [key: "Editor", value: "editor", disabled: true],
    [key: "Viewer", value: "viewer"]
  ]}
/>

Disabled options:

  • Cannot be selected via click or keyboard
  • Are skipped during arrow key navigation
  • Are skipped during typeahead matching
  • Display with reduced opacity and a not-allowed cursor
  • Have aria-disabled="true" and data-disabled attributes

Disabled options also work within grouped options:

<.select
  name="user"
  options={[
    {"Active", [
      [key: "Alice", value: "alice"],
      [key: "Bob", value: "bob"]
    ]},
    {"Inactive", [
      [key: "Charlie", value: "charlie", disabled: true],
      [key: "Diana", value: "diana", disabled: true]
    ]}
  ]}
/>

Inner Affixes

Add content inside the select's border using inner affix slots. Useful for icons, status indicators, and visual elements:

<.select name="category" options={@categories} placeholder="Select category">
  <:inner_prefix>
    <.icon name="hero-folder" class="icon" />
  </:inner_prefix>
</.select>

<.select name="status" options={@statuses} placeholder="Select status">
  <:inner_prefix>
    <.icon name="hero-flag" class="icon" />
  </:inner_prefix>
  <:inner_suffix>
    <.icon name="hero-check-circle" class="icon text-green-500!" />
  </:inner_suffix>
</.select>

Inner Suffix Replaces Chevron

When you provide an inner_suffix slot, it replaces the default chevron down arrow. The chevron is only shown when no inner_suffix is provided, maintaining the expected select behavior while allowing customization.

Outer Affixes

Place content outside the select's border using outer affix slots. Ideal for buttons, labels, and interactive elements:

<.select name="filtered" options={@options} placeholder="Select option">
  <:outer_prefix class="px-3 text-foreground-soft">Filter:</:outer_prefix>
</.select>

<.select name="with_action" options={@options} placeholder="Select option">
  <:outer_suffix>
    <.button size="md">Apply</.button>
  </:outer_suffix>
</.select>

Size Matching with Affixes

When using buttons or components within affix slots, match their size attribute to the select's size for proper visual alignment:

<.select name="example" size="lg" options={@options}>
  <:outer_suffix>
    <.button size="lg">Action</.button>
  </:outer_suffix>
</.select>

Custom Option Rendering

Customize how options appear in the dropdown through the :option slot. The component passes a {label, value} tuple to the slot via :let:

<.select
  name="role"
  placeholder="Select role"
  options={[
    {"Admin", "admin"},
    {"Editor", "editor"},
    {"Viewer", "viewer"}
  ]}
>
  <:option :let={{label, value}}>
    <div class={[
      "flex items-center justify-between",
      "rounded-lg py-2 px-3",
      "in-data-selected:bg-zinc-100",
      "in-data-highlighted:bg-zinc-50"
    ]}>
      <div>
        <div class="font-medium text-sm">{label}</div>
        <div class="text-zinc-500 text-xs">
          {case value do
            "admin" -> "Full access to all features"
            "editor" -> "Can create and modify content"
            "viewer" -> "Read-only access to content"
          end}
        </div>
      </div>
      <.icon :if={value == "admin"} name="u-shield-check" class="size-4 text-blue-500" />
    </div>
  </:option>
</.select>

Toggle Label

The toggle button always displays the option's text label regardless of custom rendering. For example, an option {"Admin User", "admin"} will show "Admin User" in the toggle when selected, even if the dropdown renders it with an avatar and description.

Option States

Custom options can respond to these data attributes for state-aware styling:

AttributeApplied WhenUse Case
[data-highlighted]Option is highlighted via keyboard or mouse hoverHover/focus background
[data-selected]Option is currently selectedCheckmark, bold text, accent color
[data-disabled]Option is disabledReduced opacity, not-allowed cursor

Use Tailwind's in-data-* variants to style ancestor-aware states:

<.select name="country" options={@countries}>
  <:option :let={{label, value}}>
    <div class={[
      "flex items-center gap-2 px-3 py-2 cursor-default select-none",
      "in-data-highlighted:bg-zinc-100",
      "in-data-selected:font-medium",
      "in-data-disabled:opacity-50"
    ]}>
      <.icon name={"flag-#{String.downcase(value)}"} class="size-5" />
      <span>{label}</span>
    </div>
  </:option>
</.select>

Add custom content at the top and bottom of the listbox using the :header and :footer slots. Useful for filter buttons, action links, or informational text:

<.select field={f[:product]} options={@products}>
  <:header class="p-2 border-b">
    <div class="flex gap-2">
      <.button size="xs" class="rounded-full" phx-click="filter" phx-value-type="all">All</.button>
      <.button size="xs" class="rounded-full" phx-click="filter" phx-value-type="active">Active</.button>
    </div>
  </:header>

  <:footer class="p-2 border-t">
    <.button type="button" size="sm" class="w-full" as="link" navigate={~p"/products/new"}>
      <.icon name="u-plus" class="size-4" /> Create new
    </.button>
  </:footer>
</.select>

Both slots accept a class attribute for custom styling.

Keyboard Navigation

The select component implements comprehensive keyboard navigation following WAI-ARIA listbox patterns:

KeyContextDescription
Tab / Shift+TabToggle buttonMoves focus to and from the select
Space / EnterToggle buttonOpens the listbox; if open, selects highlighted option
Down ArrowToggle (closed)Opens the listbox and highlights the first option (or last selected)
Up ArrowToggle (closed)Opens the listbox and highlights the last option (or last selected)
Down ArrowListbox openMoves highlight to the next visible, non-disabled option
Up ArrowListbox openMoves highlight to the previous visible, non-disabled option
HomeListbox openMoves highlight to the first visible, non-disabled option
EndListbox openMoves highlight to the last visible, non-disabled option
EnterListbox openSelects the highlighted option (closes for single, stays open for multiple)
EscapeListbox openCloses the listbox and returns focus to the toggle
BackspaceToggle buttonClears the selection (only when clearable is enabled)
Any printable characterToggle / ListboxTypeahead: finds and highlights the first matching option

When searchable is enabled, the search input receives focus when the listbox opens, and all keyboard navigation keys continue to work from the search input.

For multiple selection mode:

  • Enter toggles the selection state without closing the listbox
  • The listbox stays open for additional choices
  • Focus is maintained on the search input (when searchable) after each selection

Typeahead

When the listbox is closed, typing characters selects the matching option directly. When open, typing highlights the match. Characters accumulate for 300ms, then reset. Disabled options are skipped during typeahead.

<.form :let={f} for={@form} phx-change="validate">
  <.select field={f[:country]} options={@countries} label="Country" searchable />
</.form>
def handle_event("add_country", %{"country" => params}, socket) do
  updated_countries = [{params["name"], params["code"]} | socket.assigns.countries]
  {:noreply, assign(socket, :countries, updated_countries)}
end
<.select
  name="custom_anim"
  options={@options}
  animation="transition duration-200 ease-out"
  animation_enter="opacity-100 translate-y-0"
  animation_leave="opacity-0 -translate-y-1"
/>

Real-World Examples

Cascading Selects

Dependent select fields where selecting a value in one field determines the options in the next:

<.form :let={f} for={@location} phx-change="update">
  <.select
    field={f[:country]}
    options={@countries}
    label="Country"
    placeholder="Select a country..."
    clearable
  />
  <.select
    field={f[:state]}
    options={@states}
    label="State"
    placeholder="Select a state..."
    disabled={@states == []}
    clearable
  />
  <.select
    field={f[:city]}
    options={@cities}
    label="City"
    placeholder="Select a city..."
    disabled={@cities == []}
  />
</.form>
def mount(_params, _session, socket) do
  {:ok,
   assign(socket,
     countries: [{"United States", "us"}, {"Brazil", "br"}],
     states: [],
     cities: [],
     location: to_form(%{})
   )}
end

def handle_event("update", %{"location" => params}, socket) do
  case params do
    %{"country" => country} when country != "" ->
      states = fetch_states_for_country(country)
      {:noreply, assign(socket, states: states, cities: [], location: to_form(params))}

    %{"country" => country, "state" => state} when country != "" and state != "" ->
      cities = fetch_cities_for_state(state)
      {:noreply, assign(socket, cities: cities, location: to_form(params))}

    _ ->
      {:noreply, assign(socket, states: [], cities: [], location: to_form(params))}
  end
end

User Assignment with Avatars

<.select field={f[:assigned_to]} searchable options={@users} placeholder="Assign to...">
  <:inner_prefix>
    <.icon name="hero-user" class="icon" />
  </:inner_prefix>
  <:option :let={{label, value}}>
    <div class="flex items-center gap-3 p-2">
      <img src={"https://i.pravatar.cc/150?u=#{value}"} class="size-8 rounded-full" />
      <div>
        <div class="font-medium">{label}</div>
        <div class="text-sm text-zinc-500">{user_email(value)}</div>
      </div>
    </div>
  </:option>
</.select>

Tag Picker with Maximum Limit

<.select
  field={f[:tags]}
  multiple
  max={5}
  searchable
  clearable
  label="Tags"
  sublabel="Up to 5"
  placeholder="Select tags..."
  search_input_placeholder="Search tags..."
  options={@available_tags}
/>

Product Filter with Header Actions

<.select name="product" options={@products} placeholder="Filter products...">
  <:header class="p-2 border-b">
    <div class="flex gap-2">
      <.button size="xs" phx-click="filter" phx-value-category="all">All</.button>
      <.button size="xs" phx-click="filter" phx-value-category="active">Active</.button>
      <.button size="xs" phx-click="filter" phx-value-category="archived">Archived</.button>
    </div>
  </:header>
  <:footer class="p-2 border-t">
    <.button size="sm" class="w-full" as="link" navigate={~p"/products/new"}>
      <.icon name="u-plus" class="size-4" /> Add Product
    </.button>
  </:footer>
</.select>

Server-Side Search with Preselected Value

<.select
  field={f[:customer_id]}
  searchable
  on_search="search_customers"
  search_threshold={2}
  debounce={400}
  clearable
  label="Customer"
  search_input_placeholder="Type to search customers..."
  search_no_results_text="No customers matching '%{query}'"
  options={@customer_options}
/>
def handle_event("search_customers", %{"query" => query, "id" => _id}, socket) do
  selected_id = socket.assigns.form[:customer_id].value
  results = MyApp.Customers.search(query)

  # Always include the currently selected customer to prevent label disappearing
  options =
    if selected_id do
      selected = MyApp.Customers.get!(selected_id)
      [{selected.name, selected.id} | results] |> Enum.uniq_by(&elem(&1, 1))
    else
      results
    end

  {:noreply, assign(socket, customer_options: options)}
end

Summary

Components

Renders a select component with rich features and full keyboard navigation support.

Components

select(assigns)

Renders a select component with rich features and full keyboard navigation support.

This component provides a flexible way to build selection interfaces, from simple dropdowns to complex searchable multi-select fields with custom option rendering. Use it whenever users need to choose one or more values from a predefined list. It includes built-in form integration, error handling, and accessibility features following WAI-ARIA listbox patterns.

In its default custom mode, a hidden <select> element is maintained in sync for form submissions. Set native to render the browser's built-in <select> instead.

Attributes

  • id (:any) - Sets the unique identifier for the select component. When not provided, defaults to the name attribute value. Controls the IDs of the wrapper ({id}-wrapper), toggle button ({id}-toggle), and listbox ({id}-listbox).

    Defaults to nil.

  • name (:any) - Specifies the form field name for the select. Required when not using the field attribute. For multiple selections, the name is automatically suffixed with [] to submit an array.

  • field (Phoenix.HTML.FormField) - Binds the select to a Phoenix form field. When provided, the component automatically extracts the name, id, value, and errors from the form data, and handles changeset integration. Takes precedence over name, value, and errors attributes.

  • native (:boolean) - Controls whether to render a native HTML <select> element instead of the custom select. Native mode is useful for mobile interfaces where platform controls are preferred, or for simple selection needs. Features like search, multiple selection, clearable, custom option rendering, and affix slots are not available in native mode.

    Defaults to false.

  • class (:any) - Specifies additional CSS classes to apply to the select. For the custom select, classes are applied to the listbox container. For native selects, classes are applied to the <select> element itself.

    Defaults to nil.

  • label (:string) - Sets the primary label text displayed above the select. Renders as an HTML <label> element linked to the toggle button for accessibility.

    Defaults to nil.

  • sublabel (:string) - Specifies secondary text displayed alongside the main label. Use for contextual hints like "Required", "Optional", or "Beta" without cluttering the primary label.

    Defaults to nil.

  • help_text (:string) - Sets help text displayed below the select field. Use for supplementary guidance or instructions that assist the user in making a selection.

    Defaults to nil.

  • description (:string) - Specifies a longer description displayed below the label but above the select element. Provides additional context about what the field is used for or how the selection will be applied.

    Defaults to nil.

  • placeholder (:string) - Sets the text displayed when no option is selected. Appears as a pseudo-element via CSS when the toggle label is empty. In native mode, renders as a disabled placeholder <option>.

    Defaults to nil.

  • searchable (:boolean) - Controls whether a search input is shown at the top of the listbox for filtering options. Search is case-insensitive and diacritics-insensitive. When enabled, the search input automatically receives focus when the listbox opens. Only available in custom select mode.

    Defaults to false.

  • disabled (:boolean) - Controls whether the select is disabled. Disabled selects cannot be interacted with, appear visually muted, and are excluded from form submission.

    Defaults to false.

  • size (:string) - Controls the visual size of the select component. Adjusts height, padding, font size, and icon dimensions proportionally.

    • "xs" -- Extra small (h-7), suitable for compact UIs and dense layouts
    • "sm" -- Small (h-8), good for secondary inputs and sidebar filters
    • "md" -- Medium (h-9), the default size recommended for most forms
    • "lg" -- Large (h-10), suitable for prominent inputs and settings pages
    • "xl" -- Extra large (h-11), ideal for hero sections and onboarding flows

    Defaults to "md".

  • search_input_placeholder (:string) - Sets the placeholder text for the search input when searchable is enabled. Guides the user on what to type to filter options.

    Defaults to "Search...".

  • search_no_results_text (:string) - Specifies the message displayed when no options match the search query. Use %{query} as a placeholder token that will be replaced with the actual search term wrapped in <strong> tags.

    Defaults to "No results found for %{query}.".

  • search_threshold (:integer) - Sets the minimum number of characters required before filtering options or triggering a server-side search. Useful for preventing unnecessary operations on very short queries. A value of 0 means filtering starts immediately on the first character.

    Defaults to 0.

  • debounce (:integer) - Controls the debounce time in milliseconds for server-side searches. Delays the on_search event to avoid excessive server calls while the user is typing. Only applies when on_search is set.

    Defaults to 300.

  • on_search (:string) - Specifies the LiveView event name to trigger for server-side searching. When set, the component dispatches a fluxon:select:search custom event that the hook translates into a pushEvent(on_search, %{"query" => query, "id" => id}) call. The server should update the options assign with the filtered results.

    Defaults to nil.

  • multiple (:boolean) - Controls whether multiple options can be selected simultaneously. When enabled, the hidden <select> gains the multiple attribute, the toggle displays comma-separated labels, and selecting an option toggles it without closing the listbox. Not available in native mode.

    Defaults to false.

  • value (:any) - Sets the currently selected value or values. For single selection, pass a string or atom. For multiple selection, pass a list of values. When using the field attribute, this is automatically extracted from the form data.

  • errors (:list) - Specifies a list of error messages to display below the select. When present, the toggle button receives a data-invalid attribute for error styling. Errors are automatically extracted when using the field attribute with form validation.

    Defaults to [].

  • options (:list) (required) - Specifies the list of options for the select. Supports multiple formats following the Phoenix.HTML.Form.options_for_select/2 API:

    • Strings: ["Apple", "Banana"] -- label and value are the same
    • Tuples: [{"Label", "value"}] -- explicit label/value pairs
    • Atoms: [:admin, :user] -- converted to strings for both label and value
    • Integers: [1, 2, 3] -- converted to strings
    • Keyword lists: [key: "Admin", value: "admin"] -- with optional disabled: true
    • Keyword pairs: [Admin: "admin", User: "user"]
    • Grouped: [{"Group Label", [nested_options]}] -- supports unlimited nesting depth
  • max (:integer) - Sets the maximum number of options that can be selected when multiple is enabled. When the limit is reached, additional options become unselectable until an existing selection is removed. Defaults to nil (unlimited).

    Defaults to nil.

  • clearable (:boolean) - Controls whether selections can be cleared. When enabled, displays a clear button (X icon) in the toggle, allows re-clicking a selected option to deselect it in single-select mode, and enables Backspace to clear the selection from the toggle. For multiple select, clears all selections at once.

    Defaults to false.

  • include_hidden (:boolean) - Controls whether a hidden <input> is rendered for multiple selects. Ensures the field is always present in form submissions, even when no option is selected. Set to false only if you handle empty submissions separately.

    Defaults to true.

  • animation (:string) - Sets the base CSS transition classes for the listbox open/close animation. Defaults to "transition duration-150 ease-in-out".

  • animation_enter (:string) - Sets the CSS classes applied to the listbox when fully visible (enter animation end state). Defaults to "opacity-100 scale-100".

  • animation_leave (:string) - Sets the CSS classes applied to the listbox when closing (leave animation end state). Defaults to "opacity-0 scale-95".

  • Global attributes are accepted. Specifies additional HTML attributes passed through to the underlying <select> element. Supports the form attribute to associate with a form by ID. Supports all globals plus: ["form"].

Slots

  • option - Defines custom rendering for each option in the listbox. Receives a {label, value} tuple via :let. When not provided, options render with the default layout (label text plus a checkmark icon for selected state). Custom options can use in-data-highlighted, in-data-selected, and in-data-disabled Tailwind variants for state-aware styling.Accepts attributes:
    • class (:any) - Specifies additional CSS classes for the option wrapper.
  • toggle - Defines custom rendering for the select's trigger button. When provided, replaces the default toggle layout entirely. Use for highly customized trigger designs that don't fit the standard label-with-chevron pattern.Accepts attributes:
    • class (:any) - Specifies additional CSS classes for the toggle wrapper.
  • header - Renders custom content at the top of the listbox, above the options list. Useful for filter buttons, category tabs, or informational banners. Content appears below the search input when searchable is enabled.Accepts attributes:
    • class (:any) - Specifies additional CSS classes for the header container.
  • footer - Renders custom content at the bottom of the listbox, below the options list. Ideal for "Create new" links, summary text, or action buttons.Accepts attributes:
    • class (:any) - Specifies additional CSS classes for the footer container.
  • inner_prefix - Renders content inside the select field's border, before the selected value display. Ideal for icons or short textual prefixes that should appear within the input area. Icon sizing is automatically adjusted based on the select's size attribute.Accepts attributes:
    • class (:any) - Specifies CSS classes for the inner prefix container.
  • outer_prefix - Renders content outside and before the select field's border. Suitable for standalone labels, dropdown menus, or buttons that visually attach to the select's left edge. When containing a Fluxon.Components.Button, the border is automatically hidden to avoid double borders.Accepts attributes:
    • class (:any) - Specifies CSS classes for the outer prefix container.
  • inner_suffix - Renders content inside the select field's border, after the selected value display. Takes precedence over the default chevron icon -- when this slot is provided, the chevron is hidden. Suitable for status icons, validation indicators, or custom dropdown arrows.Accepts attributes:
    • class (:any) - Specifies CSS classes for the inner suffix container.
  • outer_suffix - Renders content outside and after the select field's border. Ideal for action buttons, submit controls, or links that visually attach to the select's right edge. When containing a Fluxon.Components.Button, the border is automatically hidden to avoid double borders.Accepts attributes:
    • class (:any) - Specifies CSS classes for the outer suffix container.