Fluxon.Components.Select (Fluxon v1.0.20)

A select component that implements a modern, accessible selection interface.

This component can be used to build both simple and complex selection interfaces. It supports single and multiple selections, option searching, custom option rendering, and keyboard navigation. The component can be used either as a custom select or as a native select element.

The component is built on top of the standard HTML <select> element. In native mode (native), it's a direct wrapper around the HTML select, while in its default custom mode, it creates an accessible UI while still using a hidden select element to handle form submissions. Multiple selection uses the HTML select multiple attribute, ensuring all form integrations (changesets, validations, etc.) work as expected.

Usage

Basic usage with a list of options:

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

The select component uses Phoenix.HTML.Form.options_for_select/2 to generate the select options. Options can be provided in these formats:

  • List of strings: ["Option 1", "Option 2"]
  • List of tuples: [{"Label 1", "value1"}, {"Label 2", "value2"}]
  • List of keyword pairs: [Label: "value"]

A full-feature example would look like this:

<.select
  name="payment_method"
  label="Payment Method"
  sublabel="Select payment type"
  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

The component defaults to a custom select interface, but you can use the native browser select element by setting native:

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

Native selects are useful for:

  • Mobile interfaces where native controls are preferred
  • Simple selection needs
  • Maximum browser compatibility
  • Performance optimization

Note that features like search, multiple selection, and clearing are only available in the custom select mode.

Form Integration

The select component integrates with Phoenix forms in two ways: using the field attribute for 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}
    errors={f[:country].errors}
  />
</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

Example with a complete 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} />
    <.select clearable multiple field={f[:languages]} options={@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"
  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

Searchable

The select component includes search functionality to filter options in large lists. Enable search by setting the searchable attribute:

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

The search runs on the client-side with these features:

  • Case-insensitive matching
  • Diacritics-insensitive (accents are ignored)
  • Filtering updates as you type
  • Keyboard navigation during search
  • Search input focus when select opens

The search matches 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
  • search_no_results_text: Text shown when no options match

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

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

Search with Multiple Selection

When using search with multiple selection, the component maintains the selected state of filtered-out options:

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

Selected options remain visible in the toggle button even when filtered out by the search. This helps users keep track of their selections while searching for additional options.

Search with Custom Options

The search functionality works with custom option rendering. The search matches against the option labels while preserving your custom rendering:

<.select 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 search functionality:

  • Runs entirely on the client side for immediate feedback
  • Uses a normalized version of the text for matching (lowercase, no accents)
  • Matches against the full option label
  • Updates the filtered list without changing the selected values
  • Maintains all keyboard navigation features during search

Multiple Selection

Enable multiple selection with the multiple attribute:

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

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

%{"_target" => ["country"], "country" => ["", "US", "CA", "MX"]}

Empty values in multiple select

According to the HTML specification, when submitting a form with a multiple select field:

  • Only selected options are included in the form submission
  • When no options are selected, the field is omitted from the form data
  • This differs from single select fields, which always submit a value

To ensure the field is always present in the form data, the component adds a hidden input with an empty value:

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

This results in consistent form submissions:

# No options selected (hidden input provides the empty value)
%{"select" => [""]}

# One or more options selected (hidden input value is included)
%{"select" => ["", "option1", "option2"]}

When processing the form data:

  1. Filter out the empty string value
  2. Handle an empty list as "no selection"
# Example processing
selected = Enum.reject(params["select"] || [], &(&1 == ""))

By default, there is no limit on the number of selections. If no options are selected, the [""] array is sent.

Maximum Selections

Use the max attribute to limit the number of selections:

<.select name="countries" multiple max={2} />

Clearable

By default, single selections cannot be unselected. The clearable attribute adds this ability and shows a clear button. Pressing backspace when the select is focused also clears the selection. For multiple selections, the clear button unselects all options.

<.select name="country" clearable />

When clearing a single selection, an empty option (<option value="">) is selected, sending an empty string in the form data. This matches how multiple select handles empty states by sending ([""]).

Custom Option Rendering

The select component supports custom option rendering through its :option slot. For each option in the list, the component passes a tuple {label, value} to the slot, which can be accessed using the :let binding:

<.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",
      "[[data-selected]_&]:bg-zinc-100",
      "[[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

While you can customize how options are rendered in the select, the toggle button will always display the option's label. This ensures consistent behavior. For example, if you have an option {"Admin User", "admin"} with custom rendering, the toggle will show "Admin User" when selected, regardless of how the option is rendered in the select.

Option States

Custom options can respond to these states using data attributes:

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

Example of styling these states:

<.select options={@countries}>
  <:option :let={{label, value}}>
    <div class={[
      # Base styles
      "flex items-center gap-2 px-3 py-2",
      "cursor-default select-none",

      # State styles using Tailwind's state selectors
      "[[data-highlighted]_&]:bg-zinc-100 dark:[[data-highlighted]_&]:bg-zinc-800",
      "[[data-selected]_&]:font-medium",
    ]}>
      <.icon name={"flag-#{String.downcase(value)}"} class="size-5" />
      <span>{label}</span>
    </div>
  </:option>
</.select>

Rich Content Examples

User Selection with Avatar

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

Product Selection with Image and Price

<.select field={f[:product]} options={@products} searchable>
  <:option :let={{label, value}}>
    <div class="flex items-center gap-3 p-2">
      <img src={product_image_url(value)} class="size-12 rounded-lg object-cover" />
      <div class="flex-1">
        <div class="font-medium">{label}</div>
        <div class="text-sm text-zinc-500">SKU: {value}</div>
      </div>
      <div class="font-medium text-zinc-900">
        {format_price(product_price(value))}
      </div>
    </div>
  </:option>
</.select>

Keyboard Support

The select component supports keyboard navigation for accessibility:

KeyElement FocusDescription
Tab/Shift+TabToggle buttonMoves focus to and from the select
Space/EnterToggle buttonOpens/closes the select
Toggle buttonOpens select and highlights last option
Toggle buttonOpens select and highlights first option
OptionMoves highlight to previous visible option
OptionMoves highlight to next visible option
HomeOptionMoves highlight to first visible option
EndOptionMoves highlight to last visible option
Enter/SpaceOptionSelects the highlighted option
EscapeAnyCloses the select
BackspaceToggle buttonClears selection (when clearable={true})
Type charactersToggle button/OptionFinds and highlights matching option

When searchable={true}, the search input receives focus when the select opens.

For multiple selection mode (multiple={true}):

  • Space/Enter toggles the selection state of the highlighted option
  • Selected options can be deselected by highlighting and pressing Space/Enter again
  • The select stays open after selection for additional choices

LiveView Integration

The select component integrates with LiveView's real-time updates. When the options list changes in the LiveView's assigns, the component automatically updates to reflect these changes while maintaining the current selection state.

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

The options list can be updated through any LiveView event:

def handle_event("add_country", %{"country" => params}, socket) do
  # Add a new country to the list
  updated_countries = [{params["name"], params["code"]} | socket.assigns.countries]
  {:noreply, assign(socket, :countries, updated_countries)}
end

def handle_info({:country_added, new_country}, socket) do
  # Handle a PubSub broadcast about a new country
  updated_countries = [new_country | socket.assigns.countries]
  {:noreply, assign(socket, :countries, updated_countries)}
end

The component handles these updates by:

  • Preserving the current selection if selected options still exist
  • Maintaining search state and filtered results
  • Keeping the select open/closed state
  • Retaining keyboard focus and navigation

Common Use Cases

Cascading Selects

Here's how to implement dependent select fields, where selecting a value in one field affects the options in subsequent fields:

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

The LiveView updates the options based on selections:

def mount(_params, _session, socket) do
  {:ok,
   assign(socket,
     # Initial data - only countries are loaded
     countries: [{"United States", "US"}, {"Brazil", "BR"}],
     states: [],
     cities: [],
     location: to_form(%{})
   )}
end

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

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

    # When any field is cleared, reset subsequent fields
    _ ->
      {:noreply, assign(socket, states: [], cities: [], location: to_form(params))}
  end
end

This implementation:

  • Disables selects until their parent has a value
  • Uses clearable to reset the selection chain
  • Loads options when needed
  • Resets dependent fields when parent is cleared

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 selects to complex searchable multi-select fields. It includes built-in form integration, error handling, and accessibility features.

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. This text is displayed above the select and is used for accessibility purposes.

    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:

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

    Defaults to "base".

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

  • 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}.".

  • 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"]
  • 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.

  • animation (:string) - The animation style for the custom select. Defaults to "transition duration-150 ease-in-out".

  • animation_enter (:string) - CSS classes for the select enter animation. Defaults to "opacity-100 scale-100".

  • animation_leave (:string) - CSS classes for the select leave animation. Defaults to "opacity-0 scale-95".

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.Accepts attributes:
    • class (:any)