Combobox component with search/filter for PhiaUI.
A searchable selection widget that renders as a trigger button + dropdown
panel. Filtering is server-side: the parent LiveView handles the search event
and passes back a filtered options list. No client-side JS hook is required
for basic functionality.
Sub-components
combobox/1— standalone combobox (trigger + dropdown panel)form_combobox/1—Phoenix.HTML.FormField-integrated variant with hidden input and changeset error display
When to use
Use combobox/1 when a <select> element would have too many options and
users benefit from typing to filter — country pickers, user selectors,
product/SKU selectors, time-zone pickers, etc.
For simple short lists (< 10 items), use a native filter_select/1 instead.
Three-event model
The parent LiveView must handle exactly three events:
| Event | When fired | Payload |
|---|---|---|
on_toggle | Trigger button clicked | none (params ignored) |
on_search | User types in the search input | %{"query" => string} |
on_change | User selects an option | %{"value" => string} |
Complete example — country selector
defmodule MyAppWeb.ProfileLive do
use Phoenix.LiveView
@countries [
%{value: "us", label: "United States"},
%{value: "gb", label: "United Kingdom"},
%{value: "de", label: "Germany"},
%{value: "fr", label: "France"},
# ... etc.
]
def mount(_params, _session, socket) do
{:ok, assign(socket,
country: nil,
country_open: false,
country_search: ""
)}
end
def handle_event("toggle-country", _params, socket) do
{:noreply, update(socket, :country_open, &(!&1))}
end
def handle_event("search-country", %{"query" => q}, socket) do
{:noreply, assign(socket, country_search: q)}
end
def handle_event("pick-country", %{"value" => v}, socket) do
{:noreply, assign(socket,
country: v,
country_open: false,
country_search: ""
)}
end
# Filter the options server-side based on search query
defp filtered_countries(search) do
query = String.downcase(search)
Enum.filter(@countries, &String.contains?(String.downcase(&1.label), query))
end
end
<%!-- Template --%>
<.combobox
id="country-picker"
options={filtered_countries(@country_search)}
value={@country}
open={@country_open}
search={@country_search}
placeholder="Select a country..."
on_toggle="toggle-country"
on_search="search-country"
on_change="pick-country"
/>Form-integrated example
<.form for={@form} phx-submit="save">
<.form_combobox
id="timezone-picker"
field={@form[:timezone]}
options={filtered_timezones(@timezone_search)}
value={@selected_timezone}
open={@timezone_open}
search={@timezone_search}
placeholder="Select timezone..."
on_toggle="toggle-tz"
on_search="search-tz"
on_change="pick-tz"
/>
<.button type="submit">Save</.button>
</.form>Options format
Options can be passed in two formats:
# Map format (preferred)
options={[%{value: "us", label: "United States"}, ...]}
# Tuple format (compatible with Phoenix.HTML.Form select helpers)
options={[{"United States", "us"}, {"Germany", "de"}, ...]}Both formats are normalised internally to %{value: string, label: string}.
ARIA
The trigger button has aria-haspopup="listbox" and aria-expanded toggled
with the open assign. The dropdown has role="listbox" and each option has
role="option" with aria-selected. Keyboard navigation (Escape / Enter /
arrow keys) requires the optional PhiaCombobox JS hook.
Summary
Functions
Renders a combobox with a trigger button and searchable dropdown.
Renders a combobox integrated with Phoenix.HTML.FormField.
Functions
Renders a combobox with a trigger button and searchable dropdown.
The component is fully server-driven: open, search, value, and
options are all controlled by the LiveView. On each re-render, the
options list is normalised and filtered by the current search query
for cases where all options are passed statically (no server-side filtering).
For large option lists (hundreds of items), handle on_search in the LiveView
and pass a pre-filtered options list instead of relying on client-side filtering.
Attributes
id(:string) (required) - Unique combobox DOM id.value(:string) - Currently selected value string, ornilwhen nothing is selected. Defaults tonil.placeholder(:string) - Trigger button text shown when no value is selected. Defaults to"Select an option...".search_placeholder(:string) - Placeholder text for the search input inside the dropdown. Defaults to"Search...".options(:list) - List of options in either format:%{value: string, label: string}maps{label, value}2-tuples (compatible with Phoenix.HTML.Form)
Pass a pre-filtered subset when the LiveView handles
on_search.Defaults to
[].open(:boolean) - Whether the dropdown panel is currently visible. Controlled by the LiveView. Defaults tofalse.search(:string) - Current search query — used for client-side label filtering whenoptionsis static. Defaults to"".on_change(:string) -phx-clickevent name emitted when a user selects an option. The LiveView receives%{"value" => value}.Defaults to
"combobox-change".on_search(:string) -phx-changeevent name emitted by the search input. The LiveView receives%{"query" => query}. Use this to filteroptionsserver-side and re-assign.Defaults to
"combobox-search".on_toggle(:string) -phx-clickevent name emitted when the trigger button is clicked. The LiveView should toggle theopenassign:update(socket, :open, &(!&1)).Defaults to
"combobox-toggle".class(:string) - Additional CSS classes merged viacn/1. Defaults tonil.Global attributes are accepted. HTML attributes forwarded to the root div.
Renders a combobox integrated with Phoenix.HTML.FormField.
Injects a <input type="hidden"> bound to field.name so the selected
value is included in phx-submit form payloads. Changeset errors from
field.errors are displayed as destructive text below the widget.
Example
<.form_combobox
id="assignee-picker"
field={@form[:assignee_id]}
options={@team_members}
value={@selected_assignee}
open={@assignee_open}
search={@assignee_search}
placeholder="Assign to..."
on_toggle="toggle-assignee"
on_search="search-assignee"
on_change="pick-assignee"
/>Attributes
field(Phoenix.HTML.FormField) (required) -Phoenix.HTML.FormFieldstruct from@form[:field_name]. Providesid,name, anderrorsfor form integration.id(:string) (required) - Unique combobox DOM id.value(:any) - Currently selected value (string or nil). Defaults tonil.placeholder(:string) - Trigger button placeholder text. Defaults to"Select an option...".search_placeholder(:string) - Placeholder text for the search input. Defaults to"Search...".options(:list) - Options list (same format ascombobox/1). Defaults to[].open(:boolean) - Whether the dropdown is visible. Defaults tofalse.search(:string) - Current search query. Defaults to"".on_change(:string) -phx-clickevent for option selection. Defaults to"combobox-change".on_search(:string) -phx-changeevent for search input. Defaults to"combobox-search".on_toggle(:string) -phx-clickevent for the trigger button. Defaults to"combobox-toggle".class(:string) - Additional CSS classes. Defaults tonil.