PUI
Forms Select

Select

Customizable dropdown with search, grouping, keyboard navigation, and form integration.

The Select component provides a rich dropdown selection experience with built-in search, option grouping, keyboard navigation, and seamless Phoenix form integration. It supports multiple option formats including strings, tuples, and grouped options.

Import

use PUI
# or
import PUI.Select

Basic Usage

The simplest select takes an options list of strings:

<.select
id="fruit"
name="fruit"
label="Favorite Fruit"
options={["Apple", "Banana", "Cherry", "Date"]}
/>

Basic Select Demo

Custom Items

Use select_item for full control over each option's rendering:

<.select id="food" name="food" label="Select Food">
<.select_item value="pizza">
<.icon name="hero-fire" class="size-4" /> Pizza
</.select_item>
<.select_item value="sushi">
<.icon name="hero-star" class="size-4" /> Sushi
</.select_item>
</.select>

Custom Items Demo

Searchable

Enable filtering by setting searchable={true}:

<.select
id="country"
name="country"
label="Country"
placeholder="Search countries..."
searchable={true}
options={["Argentina", "Brazil", "Canada", "Denmark", "Egypt"]}
/>

Searchable Select Demo

For large option sets, set search_event. The select sends a debounced event with the query and current selection; the LiveView owns the database query and assigns the filtered options back to the component:

<.select
id="country"
name="country_id"
label="Country"
searchable={true}
search_event="search_countries"
options={@country_options}
/>
def handle_event("search_countries", %{"query" => query}, socket) do
country_options = Locations.search_countries(query, socket.assigns.selected_country_id)
{:noreply, assign(socket, :country_options, country_options)}
end

The payload also includes select_id, name, and value. Clearing the query sends an empty query so the handler can restore its default result set without clearing the selected value. Keep the selected option in the returned options when its label must remain visible. For a country/city flow, use the selected country from LiveView state when querying cities.

Server-backed Search Demo

Typing in either select dispatches a debounced LiveView event. This demo uses fixture data; a host application can replace the handler with a database query.

Country query:

City query:

Default Value

Pre-select an option using the value attribute:

<.select
id="plan"
name="plan"
label="Plan"
value="pro"
options={[{"free", "Free"}, {"pro", "Pro"}, {"enterprise", "Enterprise"}]}
/>

Option Formats

Select accepts several option formats:

String List

<.select options={["Option A", "Option B", "Option C"]} />

Tuple List (value, label)

<.select options={[{"val1", "Label One"}, {"val2", "Label Two"}]} />

Grouped Options

Organize options into categories:

<.select
id="grouped"
name="grouped"
searchable={true}
options={[
{"Fruits", ["Apple", "Banana", "Cherry"]},
{"Vegetables", [{"carrot", "Carrot"}, {"lettuce", "Lettuce"}]}
]}
/>

Grouped Options Demo

Add custom content above or below the options list:

<.select id="with-footer" name="item" searchable={true}>
<.select_item value="item-1">Item One</.select_item>
<.select_item value="item-2">Item Two</.select_item>
<:footer>
<div class="border-t border-border p-2">
<button type="button" phx-click="add-item"
class="flex items-center gap-2 text-sm text-primary">
<.icon name="hero-plus" class="size-4" /> Add New Item
</button>
</div>
</:footer>
</.select>

Form Integration

Select works seamlessly with Phoenix forms:

<.form for={@form} phx-change="validate" phx-submit="save">
<.select
field={@form[:category]}
label="Category"
searchable={true}
options={["Technology", "Design", "Business"]}
/>
<.button type="submit">Save</.button>
</.form>

Field-based errors are shown automatically once the user has interacted with the select, and you can also provide them manually:

<.select
id="category"
name="category"
label="Category"
errors={["Please choose a category."]}
options={["Technology", "Design", "Business"]}
/>

Form Integration Demo

Headless

Compose PUI.Select.Primitive for full markup and styling control. The parts keep the PUI.Select hook contract, the hidden form input, and listbox semantics without any visual classes:

<PUI.Select.Primitive.root
id="custom"
search_event="search_custom_options"
class="relative my-select"
>
<PUI.Select.Primitive.input id="custom-input" name="custom" />
<PUI.Select.Primitive.trigger id="custom-trigger" listbox_id="custom-listbox" class="my-trigger">
<PUI.Select.Primitive.value placeholder="Select an item" />
</PUI.Select.Primitive.trigger>
<PUI.Select.Primitive.content
id="custom-listbox"
trigger_id="custom-trigger"
class="aria-hidden:hidden block my-listbox"
>
<PUI.Select.Primitive.search id="custom-search" listbox_id="custom-listbox" />
<PUI.Select.Primitive.item value="a" class="my-option data-[active=true]:bg-accent">Option A</PUI.Select.Primitive.item>
</PUI.Select.Primitive.content>
</PUI.Select.Primitive.root>

When users navigate with the arrow keys, focus remains on the listbox and the current option receives data-active="true". Include a data-[active=true]:… class on each item to make that state visible.

API Reference

Select Attributes

Name Type Default Description
id string nil Unique identifier
name string nil Form field name
value string nil Currently selected value
placeholder string "Select an item" Placeholder text
options list [] Options list (strings, tuples, or grouped)
searchable boolean false Enable search/filter
search_event string nil LiveView event for server-backed search
search_debounce integer 300 Delay before dispatching the search event
label string nil Label text
field FormField nil Phoenix form field
errors list [] Error messages rendered below the select
class string "w-fit" Additional CSS classes

Select Slots

Name Required Description
inner_block Custom select items
header Content above the options list
footer Content below the options list

SelectItem Attributes

Name Type Default Description
value string required Option value
class string "" Additional CSS classes