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.
Using with Phoenix Forms (Recommended)
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 inputsearch_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:
- Filter out the empty string value
- 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:
Key | Element Focus | Description |
---|---|---|
Tab /Shift+Tab | Toggle button | Moves focus to and from the select |
Space /Enter | Toggle button | Opens/closes the select |
↑ | Toggle button | Opens select and highlights last option |
↓ | Toggle button | Opens select and highlights first option |
↑ | Option | Moves highlight to previous visible option |
↓ | Option | Moves highlight to next visible option |
Home | Option | Moves highlight to first visible option |
End | Option | Moves highlight to last visible option |
Enter /Space | Option | Selects the highlighted option |
Escape | Any | Closes the select |
Backspace | Toggle button | Clears selection (when clearable={true} ) |
Type characters | Toggle button/Option | Finds 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
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 tonil
.name
(:any
) - The form name for the select. Required when not using thefield
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 whensearchable={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 whennative={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 thefield
attribute.errors
(:list
) - List of error messages to display below the select. These are automatically handled when using thefield
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"]
- List of strings:
max
(:integer
) - Maximum number of options that can be selected whenmultiple={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 tofalse
.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
)