Fluxon.Components.Select (Fluxon v3.0.0)

Provides <.select> component for single and multiple option selection with search, keyboard navigation, and form integration.

This module renders either a custom accessible select interface or a native HTML <select> element. The custom mode uses a hidden <select> element for form submissions while presenting a rich UI with floating listbox, typeahead, client/server search, clearable selections, grouped options, and full keyboard navigation. Multiple selection uses the HTML select multiple attribute, ensuring all form integrations (changesets, validations, etc.) work as expected.

Select vs Autocomplete

Both components enable choosing from a list, but serve different interaction patterns:

FeatureSelectAutocomplete
Primary InteractionClick to browseType to search
Data SizeSmall to medium listsAny size dataset
FilteringClient or server-sideClient or server-side
Multiple SelectionSupportedNot supported
Clearing SelectionSupportedNot supported

Basic Usage

Render a select with a list of options:

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

Options can be provided in multiple formats compatible with Phoenix.HTML.Form.options_for_select/2:

  • Strings: ["Option 1", "Option 2"]
  • Tuples: [{"Label", "value"}, ...]
  • Keyword pairs: [Admin: "admin", User: "user"]
  • Keyword lists: [[key: "Admin", value: "admin"], ...]
  • Atoms or integers: [:admin, :user] or [1, 2, 3]
  • Grouped options: [{"Group", [{"Label", "value"}, ...]}, ...]
  • Nested groups: [{"Region", [{"Country", [{"City", "id"}, ...]}, ...]}, ...]

With labels, descriptions, and a 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"}
  ]}
/>

Size Variants

Scale the select for different contexts:

<.select size="xs" name="size_xs" options={@options} />
<.select size="sm" name="size_sm" options={@options} />
<.select size="md" name="size_md" options={@options} />
<.select size="lg" name="size_lg" options={@options} />
<.select size="xl" name="size_xl" options={@options} />
SizeHeightTextUse Case
xs28pxxsCompact UI elements
sm32pxsmSecondary selections
md36pxsmDefault size
lg40pxbasePrimary selections
xl44pxlgHero sections, prominent forms

Options

Grouped Options

Group related options under labels. Groups support arbitrary nesting depth, with each level receiving a CSS --depth variable for visual indentation:

<.select
  name="city"
  options={[
    {"North America", [
      {"United States", [{"New York", "ny"}, {"Los Angeles", "la"}]},
      {"Canada", [{"Toronto", "to"}, {"Vancouver", "va"}]}
    ]},
    {"Europe", [
      {"France", [{"Paris", "pa"}, {"Lyon", "ly"}]}
    ]}
  ]}
/>

Custom Option Rendering

Use the :option slot for rich content. Each option receives a {label, value} tuple:

<.select name="role" options={[{"Admin", "admin"}, {"Editor", "editor"}]}>
  <:option :let={{label, value}}>
    <div class={[
      "flex items-center justify-between rounded-lg py-2 px-3",
      "in-data-highlighted:bg-zinc-100",
      "in-data-selected:font-medium"
    ]}>
      <div>
        <div class="font-medium text-sm">{label}</div>
        <div class="text-zinc-500 text-xs">Role description here</div>
      </div>
    </div>
  </:option>
</.select>

Options expose two data attributes for styling:

  • [data-highlighted]: set when the option is highlighted via keyboard or mouse hover
  • [data-selected]: set when the option is currently selected

Toggle Label with Custom Options

The toggle button always displays the option's plain text label, regardless of how the option is rendered in the dropdown. Use the :toggle_label slot to customize how selected values appear in the toggle.

Native Select

Render a native HTML <select> element for simpler needs or mobile interfaces:

<.select name="country" native options={[{"United States", "US"}, {"Canada", "CA"}]} />

Native mode does not support search, multiple selection, custom rendering, or clearable.

Searchable

Enable client-side filtering with searchable:

<.select
  name="country"
  searchable
  search_input_placeholder="Search countries..."
  no_results_text="No countries found for %{query}."
  options={@countries}
/>

Client-side search features:

  • Case-insensitive matching against option labels
  • Search input auto-focuses when the dropdown opens
  • Keyboard navigation works within filtered results
  • Search state persists when closing and reopening

For large datasets, provide on_search 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..."
  no_results_text="No users found for '%{query}'"
  options={@filtered_users}
/>

The event receives %{"query" => query, "id" => component_id}. A loading indicator is shown while waiting for the response. The component displays an error message if the server callback fails.

Keep Selected Options in Search Results

When using server-side search, the toggle label is derived from the current options list. If a selected option is filtered out of the results, its label disappears from the toggle. Always include currently selected options in your search results to prevent this.

Search configuration attributes:

  • search_threshold: minimum characters before filtering (default: 0)
  • debounce: milliseconds to wait after typing before searching (default: 300)
  • on_search: LiveView event name for server-side search

Multiple Selection

Enable with the multiple attribute. The dropdown stays open after each selection:

<.select
  name="countries"
  multiple
  placeholder="Select countries"
  options={[{"United States", "US"}, {"Canada", "CA"}, {"Mexico", "MX"}]}
/>

Limit the number of selections with max:

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

Empty Values in Multiple Select

A hidden input with an empty value is included so the field is always present in form submissions. When no options are selected, the form data contains [""]. Filter out empty strings when processing:

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

Clearable

Add a clear button and allow deselecting the current value:

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

When clearable is enabled:

  • A clear (X) button appears in the toggle when a value is selected
  • Clicking a selected option in single mode deselects it
  • Pressing Backspace on the toggle clears the selection
  • In multiple mode, the clear button removes all selections

Affixes

Inner Affixes

Add content inside the toggle border. Icons are automatically sized to match the select's size:

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

Inner Suffix Replaces Chevron

Providing an inner_suffix slot replaces the default chevron icon.

Outer Affixes

Place content outside the toggle border for buttons, labels, or interactive elements:

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

Size Matching with Affixes

Match the size of buttons or components in affix slots with the select's size:

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

Add custom content at the top or bottom of the dropdown:

<.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>

Custom Toggle

The :toggle slot replaces the entire trigger element. The default toggle ships with built-in styling, a chevron indicator, affix support, and a clear button. None of that is included when you use this slot. You are fully responsible for the toggle's appearance, layout, and any interactive affordances (placeholder text, selected value display, open/close indicator, etc.).

The slot receives a {label, value} tuple for the current selection (label is nil when nothing is selected):

<.select name="user" options={@users}>
  <:toggle :let={{label, value}}>
    <div class="flex items-center gap-2 p-2 border rounded-lg cursor-pointer hover:border-primary">
      <img src={user_avatar(value)} class="size-6 rounded-full" />
      <span class="text-sm">{label || "Select user..."}</span>
      <.icon name="hero-chevron-down" class="ml-auto size-4 text-muted" />
    </div>
  </:toggle>
</.select>

No Default Styling

The custom toggle is a blank canvas. You must provide all styling yourself: borders, padding, hover states, focus rings, placeholder text, icons, and layout. The component only handles open/close behavior and keyboard bindings.

Custom Toggle Label

The :toggle_label slot is a lighter customization point that changes only how the selected value is displayed inside the toggle, while preserving all default toggle chrome (border, padding, chevron, clear button, affixes). This is ideal when you want a custom value display (e.g., badges, avatars, colored dots) without rebuilding the entire toggle.

The slot receives a {label, value} tuple for each selected option. In multiple mode, the slot is rendered once per selected item:

<.select name="status" options={@statuses}>
  <:toggle_label :let={{label, value}}>
    <span class="flex items-center gap-1.5">
      <span class={"size-2 rounded-full " <> status_color(value)} />
      {label}
    </span>
  </:toggle_label>
</.select>

Works well with multiple for tag-style rendering:

<.select name="tags" multiple options={@tags} clearable>
  <:toggle_label :let={{label, _value}}>
    <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-blue-100 text-blue-800">
      {label}
    </span>
  </:toggle_label>
</.select>

Form Integration

Use the field attribute to bind to a Phoenix form field for automatic value tracking, error handling, and form submission:

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

For standalone usage without a form, use the name and value attributes directly:

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

Selection and Focus

The toggle opens on Space or Arrow Down/Arrow Up, highlighting the first, last, or currently selected option. While open, Arrow Down/Arrow Up move the highlight through the visible options without wrapping, Home and End jump to the first and last, and typing any character does a typeahead highlight by label prefix. Space (or Enter) selects the highlighted option; in multiple selection mode it toggles the option and leaves the dropdown open. Enter on the closed toggle does not open the dropdown, so it stays out of the way of form submission. Escape closes the dropdown and returns focus to the toggle, and Backspace clears the selection when clearable is enabled.

When searchable is enabled, the search input takes focus on open and drives the same highlight movement, selection, and dismissal keys against the filtered results.

LiveView Integration

The component syncs with LiveView DOM patches automatically. When the server updates the options list, the component preserves selection state, search state, open/closed state, and keyboard focus. This enables patterns like cascading selects where one select's value determines another's options.

Common Patterns

Cascading Selects

<.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>

Searchable Multi-Select with Tags

<.select field={f[:tags]} multiple searchable clearable options={@tags} placeholder="Add tags...">
  <:toggle_label :let={{label, _value}}>
    <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-blue-100 text-blue-800">
      {label}
    </span>
  </:toggle_label>
</.select>

Rich Option with Custom Rendering

<.select field={f[:user]} options={@users} searchable placeholder="Select user">
  <: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>

Server-Side Search

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

Constraining the Field Width

By default, the field fills the width of its container. To narrow only the field while keeping the label, description, and help text at their natural width, apply a width utility to any ancestor that targets [data-part=field-root]:

<div class="**:data-[part=field-root]:max-w-xs">
  <.select label="Country" description="..." options={...} />
</div>

This pattern works uniformly across all Fluxon input-shaped form components.

Summary

Components

Renders a select component.

Components

select(assigns)

Renders a select component.

By default, renders a custom accessible select with floating listbox, keyboard navigation, and form integration. Set native to render a standard HTML <select> element instead.

See the module documentation for usage examples, slot descriptions, and feature details.

Examples

Basic select with placeholder:

<.select
  name="country"
  placeholder="Select a country"
  options={[{"United States", "US"}, {"Canada", "CA"}, {"Mexico", "MX"}]}
/>

Form-bound searchable multi-select:

<.select
  field={f[:languages]}
  label="Languages"
  multiple
  searchable
  clearable
  options={@languages}
/>

Attributes

  • id (:any) - The unique identifier for the select component. When not provided, a random ID will be generated. Defaults to nil.

  • name (:any) - The form name for the select. Required when not using the field attribute. For multiple selections, this will be automatically suffixed with [].

  • field (Phoenix.HTML.FormField) - The form field to bind to. When provided, the component automatically handles value tracking, errors, and form submission.

  • native (:boolean) - When true, renders a native HTML select element instead of the custom select. This is useful for simple use cases or when native mobile behavior is preferred. Note that features like search and multiple selection are not available in native mode.

    Defaults to false.

  • class (:any) - Additional CSS classes to apply to the select component. For the custom select, this affects the listbox container. For native selects, it applies to the select element.

    Defaults to nil.

  • label (:string) - The primary label for the select. Rendered above the field and associated with the toggle through a <label> element so clicking it focuses the select.

    Defaults to nil.

  • sublabel (:string) - Additional context displayed to the side of the main label. Useful for providing extra information without cluttering the main label.

    Defaults to nil.

  • help_text (:string) - Help text to display below the select. This can provide additional context or instructions for using the select.

    Defaults to nil.

  • description (:string) - A longer description to provide more context about the select. This appears below the label but above the select element.

    Defaults to nil.

  • placeholder (:string) - Text to display when no option is selected. This text appears in the select toggle and helps guide users to make a selection.

    Defaults to nil.

  • searchable (:boolean) - When true, adds a search input to filter options. The search is case and diacritics insensitive. Only available for custom selects.

    Defaults to false.

  • disabled (:boolean) - When true, disables the select component. Disabled selects cannot be interacted with and appear visually muted.

    Defaults to false.

  • size (:string) - Controls the size of the select component:

    • "xs": Extra small size, suitable for compact UIs
    • "sm": Small size, suitable for compact UIs
    • "md": Default size, suitable for most use cases
    • "lg": Large size, suitable for prominent selections
    • "xl": Extra large size, suitable for hero sections

    Defaults to "md". Must be one of "xs", "sm", "md", "lg", or "xl".

  • search_input_placeholder (:string) - Placeholder text for the search input when searchable={true}. Defaults to "Search...".

  • no_results_text (:string) - Text to display when no options match the search query. Use %{query} as a placeholder for the actual search term.

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

  • search_no_results_text (:string) - Deprecated alias for no_results_text. Setting this attribute logs a warning at render time. Switch to no_results_text instead.

    Defaults to nil.

  • search_threshold (:integer) - The minimum number of characters required before filtering options or performing server searches. This helps prevent unnecessary operations for very short queries.

    Defaults to 0.

  • debounce (:integer) - The debounce time in milliseconds for server-side searches. This delays the on_search event to avoid excessive API calls while the user is typing.

    Defaults to 300.

  • on_search (:string) - Name of the LiveView event to be triggered when searching. If provided, filtering will be handled server-side. The event receives %{"query" => query} as parameters.

    Defaults to nil.

  • on_open (Phoenix.LiveView.JS) - Phoenix.LiveView.JS commands to run after the listbox finishes opening (after the enter animation completes).

    Defaults to %Phoenix.LiveView.JS{ops: []}.

  • on_close (Phoenix.LiveView.JS) - Phoenix.LiveView.JS commands to run after the listbox finishes closing (after the leave animation completes).

    Defaults to %Phoenix.LiveView.JS{ops: []}.

  • multiple (:boolean) - When true, allows selecting multiple options. This changes the behavior to use checkboxes in the select and submits an array of values. Not available when native={true}.

    Defaults to false.

  • value (:any) - The current selected value(s). For multiple selections, this should be a list. When using forms, this is automatically handled by the field attribute.

  • errors (:list) - List of error messages to display below the select. These are automatically handled when using the field attribute with form validation.

    Defaults to [].

  • options (:list) (required) - A list of options for the select. Can be provided in multiple formats:

    • List of strings: ["Option 1", "Option 2"]
    • List of tuples: [{"Label 1", "value1"}, {"Label 2", "value2"}]
    • List of keyword pairs: [key: "value"]
    • Keyword list with disabled: [[key: "Label", value: "val", disabled: true]]
    • Grouped options: [{"Group 1", ["Option 1", "Option 2"]}, {"Group 2", [{"Label 1", "value1"}]}]
    • Range: 1..12 or 1..12//-1

    Disabled options are visually muted, cannot be selected, and are skipped during keyboard navigation.

  • max (:integer) - Maximum number of options that can be selected when multiple={true}. When reached, other options become unselectable.

    Defaults to nil.

  • clearable (:boolean) - When true, this option displays a clear button to remove the current selection(s). It also allows users to clear the selection by clicking on the selected option in non-multiple selects. Defaults to false.

  • include_hidden (:boolean) - When true, includes a hidden input for the select. This ensures the field is always present in form submissions, even when no option is selected.

    Defaults to true.

  • Global attributes are accepted. Additional attributes to pass to the select element. Supports all globals plus: ["form"].

Slots

  • option - Optional slot for custom option rendering. When provided, each option can be fully customized with rich content.Accepts attributes:
    • class (:any)
  • toggle - Optional slot for custom toggle rendering. This allows complete customization of the select's trigger button. When provided, this slot takes precedence over :toggle_label and default rendering.Accepts attributes:
    • class (:any) - Additional CSS classes to apply to the toggle wrapper.
  • toggle_label - Optional slot for customizing how selected options are displayed in the toggle's label area. Preserves all default toggle styling (affixes, clear button, chevron). Receives {label, value} tuple for each selected option.This provides an intermediate customization level between plain text (default) and full toggle customization (:toggle slot). Ignored when native={true}.Accepts attributes:
    • class (:any) - Additional CSS classes for the toggle label container.
  • header - Optional slot for custom header rendering. When provided, this content is shown in the top of the listbox.Accepts attributes:
    • class (:any) - Additional CSS classes to apply to the header.
  • footer - Optional slot for custom footer rendering. When provided, this content is shown in the bottom of the listbox.Accepts attributes:
    • class (:any) - Additional CSS classes to apply to the footer.
  • inner_prefix - Content placed inside the select field's border, before the selected value display. Ideal for icons or short textual prefixes. Can be used multiple times.Accepts attributes:
    • class (:any) - CSS classes for styling the inner prefix container.
  • outer_prefix - Content placed outside and before the select field. Useful for buttons, dropdowns, or other interactive elements associated with the select's start. Can be used multiple times.Accepts attributes:
    • class (:any) - CSS classes for styling the outer prefix container.
  • inner_suffix - Content placed inside the select field's border, after the selected value display. Suitable for icons, clear buttons, or loading indicators. Takes precedence over the default chevron icon. Can be used multiple times.Accepts attributes:
    • class (:any) - CSS classes for styling the inner suffix container.
  • outer_suffix - Content placed outside and after the select field. Useful for action buttons or other controls related to the select's end. Can be used multiple times.Accepts attributes:
    • class (:any) - CSS classes for styling the outer suffix container.