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:
| Feature | Select | Autocomplete |
|---|---|---|
| Primary Interaction | Click to browse | Type to search |
| Data Size | Small to medium lists | Any size dataset |
| Filtering | Client or server-side | Client or server-side |
| Multiple Selection | Supported | Not supported |
| Clearing Selection | Supported | Not 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} />| Size | Height | Text | Use Case |
|---|---|---|---|
xs | 28px | xs | Compact UI elements |
sm | 32px | sm | Secondary selections |
md | 36px | sm | Default size |
lg | 40px | base | Primary selections |
xl | 44px | lg | Hero 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
Server-Side Search
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
Backspaceon 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>Header and Footer
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
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 tonil.name(:any) - The form name for the select. Required when not using thefieldattribute. 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 whensearchable={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 forno_results_text. Setting this attribute logs a warning at render time. Switch tono_results_textinstead.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 theon_searchevent 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.JScommands 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.JScommands 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 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 thefieldattribute.errors(:list) - List of error messages to display below the select. These are automatically handled when using thefieldattribute 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..12or1..12//-1
Disabled options are visually muted, cannot be selected, and are skipped during keyboard navigation.
- 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.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_labeland 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 (:toggleslot). Ignored whennative={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.