Autocomplete

Searchable single-select combobox with grouped options and custom row templates.

Basic

.heex
<.autocomplete
  name="provider"
  label="Streaming provider"
  placeholder="Search providers..."
  options={[
    {"Netflix", "netflix"},
    {"Amazon Prime Video", "amazon"},
    {"HBO Max", "hbo"},
    {"Apple TV+", "apple"},
    {"Paramount+", "paramount"},
    {"Disney+", "disney"}
  ]}
/>

Label, description, and help text

Combine label, sublabel, description, and help_text to build the full form-field chrome around the input.

Required

Tailors the dashboard and report set we surface for you.

You can change this later from account settings.
.heex
<.autocomplete
  name="role"
  label="Job role"
  sublabel="Required"
  description="Tailors the dashboard and report set we surface for you."
  help_text="You can change this later from account settings."
  placeholder="Select a role..."
  options={@roles}
/>

Sizes

.heex
<.autocomplete name="genre_xs" size="xs" label="Genre (xs)" options={@genres} />
<.autocomplete name="genre_sm" size="sm" label="Genre (sm)" options={@genres} />
<.autocomplete name="genre_md" size="md" label="Genre (md)" options={@genres} />
<.autocomplete name="genre_lg" size="lg" label="Genre (lg)" options={@genres} />
<.autocomplete name="genre_xl" size="xl" label="Genre (xl)" options={@genres} />

Grouped and nested options

Wrap options in {group_label, children} tuples. Groups can nest to any depth, and empty groups are hidden automatically while filtering.

.heex
<.autocomplete
  name="city"
  label="Office location"
  placeholder="Search cities..."
  options={[
    {"Europe",
     [
       {"France", [{"Paris", "paris"}, {"Lyon", "lyon"}]},
       {"Germany", [{"Berlin", "berlin"}, {"Munich", "munich"}]}
     ]},
    {"Americas",
     [
       {"United States", [{"New York", "nyc"}, {"San Francisco", "sf"}]},
       {"Canada", [{"Toronto", "toronto"}, {"Vancouver", "vancouver"}]}
     ]},
    {"Asia",
     [
       {"Japan", [{"Tokyo", "tokyo"}, {"Osaka", "osaka"}]},
       {"Singapore", [{"Singapore", "singapore"}]}
     ]}
  ]}
/>

Search modes

search_mode picks how the typed query matches each label during client-side filtering. Always case insensitive.

.heex
<.autocomplete name="contains" search_mode="contains" options={@languages} />
<.autocomplete name="starts_with" search_mode="starts-with" options={@languages} />
<.autocomplete name="exact" search_mode="exact" options={@languages} />

Search threshold

Set search_threshold to require a minimum number of characters before the listbox opens or filtering runs.

.heex
<.autocomplete
  name="actor"
  placeholder="Type at least 2 characters..."
  search_threshold={2}
  options={@actors}
/>

Listbox height

The listbox is capped at max-h-72 and shrinks to fit the space available, only opening on the other side once that space gets too small. Pass another max-h-* through class to change the cap, or max-h-none to use all the space on whichever side it opens. The class also lands on the input wrapper, so keep the cap large enough to be irrelevant to a single-line field.

.heex
<.autocomplete name="city" options={@cities} />
<.autocomplete name="city" options={@cities} class="max-h-96" />
<.autocomplete name="city" options={@cities} class="max-h-none" />

Open on focus

open_on_focus opens the listbox the moment the input gets focus, even with an empty query.

.heex
<.autocomplete
  name="department"
  label="Department"
  open_on_focus
  options={@departments}
/>

Clearable selection

Pass clearable to render a clear button inside the input whenever a value is selected. Clearing resets the selection and re-shows every option.

.heex
<.autocomplete
  name="director"
  label="Director"
  clearable
  value="nolan"
  options={@directors}
/>

Inner affixes

:inner_prefix and :inner_suffix sit inside the input border, so icons like a leading search glyph travel with the field.

.heex
<.autocomplete name="movie" placeholder="Search movies..." options={@movies}>
  <:inner_prefix>
    <.icon name="hero-magnifying-glass" class="icon" />
  </:inner_prefix>
  <:inner_suffix>
    <.icon name="hero-tv" class="icon" />
  </:inner_suffix>
</.autocomplete>

Outer affixes (input group)

:outer_prefix and :outer_suffix sit beside the field, sharing borders with it to form a single input group. Match the affix child's size to the autocomplete's.

Login
.heex
<.autocomplete name="invite" placeholder="Search teammates..." options={@users}>
  <:inner_prefix>
    <.icon name="hero-magnifying-glass" class="icon" />
  </:inner_prefix>
  <:outer_suffix>
    <.button>
      <.icon name="hero-user-plus" class="icon" /> Invite
    </.button>
  </:outer_suffix>
</.autocomplete>

<.autocomplete name="provider" placeholder="Search providers..." options={@providers}>
  <:outer_prefix class="px-3 font-medium text-foreground bg-sunken/40">
    Login
  </:outer_prefix>
</.autocomplete>

:header and :footer stay pinned in the listbox regardless of the active query. Drop filter chips above the list, or a 'create new' action below it.

Custom option rendering

Pass an :option slot to fully control how each row renders. The {label, value} tuple is yielded to the slot, and the wrapping element exposes data-highlighted and data-selected for styling.

.heex
<.autocomplete field={f[:user_id]} options={Enum.map(@users, &{&1.full_name, &1.id})} on_search="search_users">
  <:option :let={{label, value}}>
    <div class="flex items-center gap-3 p-2 rounded-lg in-data-highlighted:bg-zinc-100 in-data-selected:bg-blue-100">
      <img src={"https://i.pravatar.cc/150?u=#{value}"} class="size-8 rounded-full" />
      <div>
        <div class="font-medium text-sm">{label}</div>
        <div class="text-xs text-zinc-500">{user_by_id(value, @users).email}</div>
      </div>
    </div>
  </:option>
</.autocomplete>

Custom empty state

Provide an :empty_state slot to replace the default text-only 'no results' message with a richer block. Falls back to no_results_text when the slot is omitted.

Search company departments

.heex
<.modal id="new-department">
  <%!-- Form that submits to save_department --%>
</.modal>

<.autocomplete
  field={@form[:department]}
  label="Department"
  placeholder="Select a department"
  description="Search company departments"
  options={@departments}
>
  <:empty_state>
    <.button
      class="w-full"
      variant="ghost"
      size="sm"
      type="button"
      phx-click={Fluxon.open_dialog("new-department")}
    >
      <.icon name="huge-add-01" class="size-4" /> Create new department
    </.button>
  </:empty_state>
</.autocomplete>
.ex
def handle_event("save_department", %{"name" => _name}, socket) do
  # Persist the new department and refresh the options assign
  {:noreply, Fluxon.close_dialog(socket, "new-department")}
end

Pass on_search to hand filtering to a LiveView event. The component dispatches the event with %{"query" => query, "id" => component_id} and shows a loading indicator while the LiveView assigns fresh options.

Search debouncing

debounce sets the milliseconds the component waits between keystrokes before dispatching on_search. Keeps request volume down on busy fields.

.heex
<.autocomplete
  name="location_search"
  label="Filming Location"
  placeholder="Search filming locations..."
  debounce={1000}
  options={@location_options}
  on_search="search_locations"
/>
.ex
def handle_event("search_locations", %{"query" => query}, socket) do
  send(self(), {:search_locations, query})
  {:noreply, assign(socket, searching_locations: true)}
end

def handle_info({:search_locations, _query}, socket) do
  # Run the search and reassign location_options
  {:noreply, assign(socket, searching_locations: false)}
end

Form integration

Bind to a Phoenix form field with field={f[:assigned_to_id]}. Name, value, id, and validation errors are derived from the form automatically.

.heex
<.form :let={f} for={@form} phx-change="validate" phx-submit="save">
  <.autocomplete
    field={f[:genre]}
    label="Movie Genre"
    options={@genres}
    placeholder="Select genre..."
  />

  <.autocomplete
    field={f[:studio]}
    label="Production Studio"
    options={@studios}
    placeholder="Choose studio..."
  />

  <.button type="submit">Save Movie</.button>
</.form>
.ex
def handle_event("validate", %{"movie" => _params}, socket) do
  # Re-run validation and reassign the form
  {:noreply, socket}
end

def handle_event("save", %{"movie" => _params}, socket) do
  # Persist the movie, then redirect or assign success state
  {:noreply, socket}
end

Disabled

.heex
<.autocomplete
  name="frozen"
  label="Locked field"
  disabled
  value="bafta"
  options={@awards}
/>

Validation errors

Pass errors as a list of strings to render messages and apply invalid styling. Bound forms populate this automatically from the changeset.

.heex
<.autocomplete
  name="rating"
  label="Movie rating"
  errors={["This field is required"]}
  options={@ratings}
/>

<.autocomplete
  name="advisory"
  label="Content advisory"
  errors={["This field is required", "Pick at least one advisory"]}
  options={@advisories}
/>