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
<div class="max-w-sm">
<.select
id="demo-basic"
name="demo-basic"
label="Favorite Fruit"
options={[
"Apple",
"Banana",
"Cherry",
"Date",
"Elderberry",
"Pisang",
"Melon",
"Anggur",
"Manggis",
"Kelapa",
"Jambu",
"Salak",
"Semangka",
"Alpukat",
"Tamarin",
"Nangka",
"Durian"
]}
/>
</div>
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
<div class="max-w-sm">
<.select id="demo-custom" name="demo-custom" label="Select Action">
<.select_item value="edit">
<.icon name="hero-pencil" class="size-4" /> Edit
</.select_item>
<.select_item value="duplicate">
<.icon name="hero-document-duplicate" class="size-4" /> Duplicate
</.select_item>
<.select_item value="archive">
<.icon name="hero-archive-box" class="size-4" /> Archive
</.select_item>
</.select>
</div>
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
<div class="max-w-sm">
<.select
id="demo-search"
name="demo-search"
label="Search Countries"
placeholder="Type to search..."
searchable={true}
options={[
"Argentina",
"Brazil",
"Canada",
"Denmark",
"Egypt",
"France",
"Germany",
"India",
"Japan"
]}
/>
</div>
Server-backed Search
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:
<p class="max-w-xl text-sm text-muted-foreground">
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.
</p>
<div class="grid max-w-2xl gap-4 md:grid-cols-2">
<.select
id="docs-remote-country"
name="country_id"
label="Country"
value="in"
searchable={true}
search_event="search_demo_countries"
options={@country_options}
/>
<.select
id="docs-remote-city"
name="city_id"
label="City"
searchable={true}
search_event="search_demo_cities"
options={@city_options}
/>
</div>
<div class="space-y-1 text-sm text-muted-foreground">
<p id="docs-remote-country-query">Country query: {@country_query}</p>
<p id="docs-remote-city-query">City query: {@city_query}</p>
</div>
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
<div class="max-w-sm">
<.select
id="demo-grouped"
name="demo-grouped"
label="Select Food"
searchable={true}
options={[
{"Fruits", ["Apple", "Banana", "Cherry"]},
{"Vegetables", [{"carrot", "Carrot"}, {"lettuce", "Lettuce"}, {"tomato", "Tomato"}]}
]}
/>
</div>
Header and Footer Slots
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
<.form for={@form} phx-change="validate" class="max-w-sm">
<.select
field={@form[:select]}
label="Favorite Fruit"
placeholder="Choose one"
options={["Apple", "Banana", "Cherry", "Date"]}
/>
</.form>
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 |