Flicker.palette/1 is a ⌘K fullscreen command-palette overlay: backdrop,
centred panel, large search input, grouped result list, and a footer of
keyboard hints (↑↓ navigate · ↵ select · esc close). It wraps the exact
same core Flicker.select/1 runs on — same provider tiers, same
Flicker.Result/Flicker.Provider contract — as a modal overlay instead
of an inline combobox. There's no special machinery behind it: it's
activate_with_keyboard + a federated provider + theme parts arranged
into one component.
Basic usage
<Flicker.palette
id="cmdk"
source={MyApp.Search.Global}
actor={@current_user}
open={@palette_open}
on_close={:palette_closed}
on_select={:palette_selected}
/>def handle_info(:palette_closed, socket), do: {:noreply, assign(socket, :palette_open, false)}
def handle_info({:palette_selected, _result}, socket), do: {:noreply, assign(socket, :palette_open, false)}on_close and on_select are both required — there's no form-field mode
for a modal overlay, only controlled mode. The host receives a bare
on_close message (send(self(), on_close), no payload) whenever the
overlay closes, and {on_select, %Flicker.Result{}} on selection.
Opening it
Two independent ways to open the palette, usable together:
activate_with_keyboard(defaults to"mod+k") — self-contained, needs no host state. The chord opens/closes the overlay entirely from the client side, the same activation patternFlicker.select/1uses.- The
open/on_closecontrolled pair — for a navbar button or any other host-driven trigger.openis adopted whenever it changes between renders: a rising edge opens the overlay, a falling edge closes it. In between, the component is free to open or close itself (the chord,Escape, a backdrop click) and always fireson_closeso host state never drifts out of sync with what's actually on screen.
<button phx-click="open_palette">Search</button>
<Flicker.palette
id="cmdk"
source={MyApp.Search.Global}
actor={@current_user}
open={@palette_open}
on_close={:palette_closed}
on_select={:palette_selected}
activate_with_keyboard="mod+k"
/>def handle_event("open_palette", _params, socket), do: {:noreply, assign(socket, :palette_open, true)}Federated search with groups
The palette's value comes from a source that searches more than one
resource in a single call, tagging each Flicker.Result with :group so
the result list renders contiguous group headers. Grouping is entirely
the provider's own choice — the palette never re-sorts by group, it just
draws a header before the first result of each new group as encountered:
defmodule MyApp.Search.Global do
@behaviour Flicker.Provider
alias Flicker.{Query, Result}
@impl true
def search(%Query{text: text}, opts) do
actor = Keyword.get(opts, :actor)
artists = MyApp.Artist |> search_by(:name, text) |> Ash.read!(actor: actor)
albums = MyApp.Album |> search_by(:title, text) |> Ash.read!(actor: actor)
results =
Enum.map(artists, &to_result(&1, "Artists", &1.name)) ++
Enum.map(albums, &to_result(&1, "Albums", &1.title))
{:ok, results}
end
@impl true
def fetch(_values, _opts), do: {:ok, []}
defp to_result(record, group, label) do
%Result{
value: record.id,
label: label,
group: group,
meta: %{href: ~p"/#{group |> String.downcase()}/#{record.id}"}
}
end
endNavigate on select
A result whose meta.href is set additionally navigates
(push_navigate/2) when it's chosen — the raw on_select message still
fires either way, so a host doing something other than navigating
(closing the palette, logging, a custom side effect) always can. Omit
meta.href for a result that should only fire on_select — e.g. running
an in-page action instead of going anywhere.
Theming
Flicker.palette/1 uses the same Flicker.Theme as Flicker.select/1,
plus five overlay-specific parts: :backdrop, :panel, :palette_input
(in place of :search_input), :group_header, and :footer. See the
theming guide for the full part list and preset system.
See also
Flicker.palette/1— the full moduledoc.- Providers — writing a federated
Flicker.Providerlike the one above. - Faceted search — the same
facetsattr narrows a palette's free-text search too.