We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Listbox
A single-select value picker with keyboard navigation, intelligent positioning, and Phoenix form integration. Use a listbox when users need to select one value from a predefined set of options.
Unlike Dropdown, which provides an action menu with role="menu",
Listbox
uses role="listbox"
and works like a native <select>. It includes smart positioning with
Floating UI
, and its options match the trigger's width by default.
Quick Start
The most basic listbox requires three components: .listbox, .listbox_trigger, and
.listbox_options
with .listbox_option
elements.
<.listbox id="basic-listbox" name="favorite_fruit" value="Cherry">
<.listbox_trigger
id="basic-listbox-trigger"
class="w-56 inline-flex justify-between items-center rounded-lg bg-white border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
>
<.listbox_value>Cherry</.listbox_value>
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
clip-rule="evenodd"
/>
</svg>
</.listbox_trigger>
<.listbox_options
id="basic-listbox-options"
class="py-1 rounded-md bg-white shadow-xs ring-1 ring-gray-300 focus:outline-none"
>
<.listbox_option
:for={
{fruit, index} <- Enum.with_index(["Cherry", "Kiwi", "Grapefruit", "Orange", "Banana"])
}
id={"basic-listbox-option-#{index}"}
value={fruit}
class="text-gray-700 data-focus:bg-gray-100 data-focus:text-gray-900 data-selected:font-semibold block w-full px-4 py-2 text-sm text-left"
>
{fruit}
</.listbox_option>
</.listbox_options>
</.listbox>
This example starts with "Cherry" selected through the value
attribute. Selecting another option updates the .listbox_value
content immediately.
Form Integration
.listbox
renders a hidden <input>
with the given name. Selecting an option updates the input and dispatches an
input
event. This makes a parent form's phx-change
event run as it does for a native form field.
Use .listbox_value
to render the current value or a placeholder in the trigger.
Selected fruit (from server state): none
defmodule DemoWeb.DemoLive.ListboxFormDemo do
@moduledoc false
use DemoWeb, :live_component
import Prima.Listbox
@fruits ["Cherry", "Kiwi", "Grapefruit", "Orange", "Banana"]
@impl true
def mount(socket) do
{:ok, assign(socket, fruits: @fruits, selected_fruit: nil)}
end
@impl true
def render(assigns) do
~H"""
<div>
<form phx-change="favorite_fruit_changed" phx-target={@myself}>
<.listbox id="demo-form-listbox" name="favorite_fruit" value={@selected_fruit}>
<.listbox_trigger
id="demo-form-listbox-trigger"
class="w-56 inline-flex justify-between items-center rounded-lg bg-white border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
>
<.listbox_value>{@selected_fruit || "Select a fruit..."}</.listbox_value>
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
clip-rule="evenodd"
/>
</svg>
</.listbox_trigger>
<.listbox_options
id="demo-form-listbox-options"
class="py-1 rounded-md bg-white shadow-xs ring-1 ring-gray-300 focus:outline-none"
>
<.listbox_option
:for={{fruit, index} <- Enum.with_index(@fruits)}
id={"demo-form-listbox-option-#{index}"}
value={fruit}
class="text-gray-700 data-focus:bg-gray-100 data-focus:text-gray-900 data-selected:font-semibold block w-full px-4 py-2 text-sm text-left"
>
{fruit}
</.listbox_option>
</.listbox_options>
</.listbox>
</form>
<p class="mt-4 text-sm text-gray-600">
Selected fruit (from server state):
<span class="font-semibold text-gray-900">{@selected_fruit || "none"}</span>
</p>
</div>
"""
end
@impl true
def handle_event("favorite_fruit_changed", %{"favorite_fruit" => fruit}, socket) do
{:noreply, assign(socket, selected_fruit: fruit)}
end
end
The selected fruit below the trigger comes from server state. It confirms that the
phx-change
event completed while the trigger value updated immediately.
Disabled Options
Disable a listbox option with the disabled=true
attribute. Disabled options cannot receive focus or be selected.
<.listbox id="disabled-demo-listbox" name="plan">
<.listbox_trigger
id="disabled-demo-listbox-trigger"
class="w-56 inline-flex justify-between items-center rounded-lg bg-white border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50"
>
<.listbox_value>Select a plan...</.listbox_value>
<svg
class="h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
clip-rule="evenodd"
/>
</svg>
</.listbox_trigger>
<.listbox_options
id="disabled-demo-listbox-options"
class="py-1 rounded-md bg-white shadow-xs ring-1 ring-gray-300 focus:outline-none"
>
<.listbox_option
id="disabled-demo-option-basic"
value="Basic"
class="text-gray-700 data-focus:bg-gray-100 data-focus:text-gray-900 block w-full px-4 py-2 text-sm text-left"
>
Basic
</.listbox_option>
<.listbox_option
id="disabled-demo-option-pro"
value="Pro"
class="text-gray-700 data-focus:bg-gray-100 data-focus:text-gray-900 block w-full px-4 py-2 text-sm text-left"
>
Pro
</.listbox_option>
<.listbox_option
id="disabled-demo-option-enterprise"
value="Enterprise"
disabled={true}
class="text-gray-400 data-disabled:opacity-50 block w-full px-4 py-2 text-sm text-left"
>
Enterprise (contact sales)
</.listbox_option>
</.listbox_options>
</.listbox>
Keyboard Interaction
The listbox provides keyboard navigation that follows standard ARIA patterns. Use the arrow keys to move between options and Enter or Space to select an option.
| Key | Description |
|---|---|
| When the trigger button is focused: | |
| Enter / Space | Opens the listbox and focuses the current selection (or the first option, if none) |
| ↓ | Opens the listbox and focuses the first non-disabled option |
| ↑ | Opens the listbox and focuses the last non-disabled option |
| When the listbox is open: | |
| Esc | Closes the listbox without changing the selection, and returns focus to the trigger |
| ↑ / ↓ | Focuses the previous/next non-disabled option (wraps around) |
| Home / End | Focuses the first/last non-disabled option |
| Enter / Space | Selects the focused option, updates the displayed value, and closes the listbox |
| A-Z / 0-9 | Focuses the first option that starts with the typed character. Repeated presses cycle through matching options. |