Ash-native search, filtering, and record selection for Phoenix LiveView.
Searchable selects, multi-selects, faceted filter bars, and command palettes
backed directly by your data — no options plumbing required.
Flicker is what Cinder is for tables, but
for finding and picking records. Point it at an Ash resource, pass the actor,
and get an authorised typeahead without loading every record into a giant
options list.
Important
Flicker is under active development and has not been published to Hex yet. Install it from GitHub for evaluation. The public API may change before the first release.
- Your resource is the data source. Give Flicker
resource,search, andoption_label; it builds and runs the Ash query itself. - Authorization stays in Ash.
actorandtenantflow through every search and fetch. Flicker adds no parallel permission system. - Small core, clean escape hatch. Phoenix LiveView is the only hard
runtime dependency. Ash support is optional, and any module implementing
Flicker.Providercan search an API, cache, index, or several resources. - One interaction model, four surfaces. Single-select, multi-select, standalone faceted search, and a full command palette share the same query, provider, result, theming, and keyboard machinery.
- Built for production details. Debounced async search, stale-result cancellation, batch resolution of existing values, reconnect-safe form inputs, windowed pagination, actor-scoped relationship facets, and user-facing error states are already covered.
- Accessible by design. Editable-combobox semantics, active-descendant keyboard navigation, polite live-region announcements, a palette focus trap, overridable copy, axe-core scans, and real-browser keyboard tests ship in the repository. Manual assistive-technology testing is still in progress; see Accessibility.
| Surface | Use it for | Entry point |
|---|---|---|
| Searchable select | Pick one record in a form or controlled LiveView | Flicker.select/1 |
| Multi-select | Pick many records with chips or custom selected-item UI | Flicker.select/1 with multiple |
| Faceted search | Drive an Ash query, Cinder collection, stream, or list | Flicker.search/1 |
| Command palette | Grouped global search with optional navigation | Flicker.palette/1 |
The current implementation also includes:
- form-field and controlled modes;
- custom option and selected-item slots;
- avatar stacks with
max_visibleoverflow; min_chars, debouncing, limits, base filters, sorting, and custom read actions;- opt-in infinite scroll with capped result windows;
- Datadog-style facets such as
status:active after:7d rock; - type-derived enum, boolean, date, number, and relationship suggestions;
- removable facet pills in
Flicker.search/1, including host-defined colour indicators; - global keyboard activation such as
mod+k; - vanilla, Tailwind, and daisyUI theme presets;
- fully overridable visible text and screen-reader announcements;
- Cinder query and URL-state helpers;
- an AshPhoenix form adapter and PhoenixTest helper;
- an Igniter-powered installer; and
- an in-repository playground with copyable examples.
See the spec index for shipped work, work in progress,
and deliberately deferred ideas. In particular, Flicker.select/1 already
parses facets and filters its records, but committed facets remain raw input
text there; pill rendering inside selects is still a draft.
Until the first Hex release, add the GitHub dependency:
def deps do
[
{:flicker, github: "team-alembic/flicker", branch: "main"},
# Required only for the built-in Ash resource provider:
{:ash, "~> 3.0"},
{:ash_phoenix, "~> 2.0"}
]
endFlicker requires Elixir 1.17 or later and Phoenix LiveView 1.1 or later. Its JavaScript is delivered through LiveView's colocated-hook dependency manifest.
If your project has Igniter, fetch the dependency and run the included installer:
mix deps.get
mix flicker.installIt imports Flicker into the formatter, adds the default limit and debounce
configuration, and merges Flicker's colocated hooks into your LiveSocket.
For manual installation, follow the exact hook wiring in the
getting-started guide.
<Flicker.select
id="client-select"
field={@form[:client_id]}
resource={MyApp.Client}
actor={@current_user}
search={[:first_name, :last_name, :uci_number]}
option_label={:full_name}
option_sublabel={fn client -> "#{client.uci_number} · #{client.city}" end}
read_action={:search}
limit={20}
/>Pass a Phoenix.HTML.FormField for form mode, or replace field with an
on_select message name for controlled mode:
<Flicker.select
id="global-search"
source={MyApp.Search.Global}
actor={@current_user}
on_select={:result_selected}
/>def handle_info({:result_selected, result}, socket) do
{:noreply, assign(socket, :selected, result)}
endExactly one of field or on_select is required.
The same component becomes a multi-select with multiple. Existing values
are resolved in one fetch/2 call, form mode emits array inputs, and
controlled mode receives the complete selection list after every change.
<Flicker.select
id="worker-select"
field={@form[:worker_ids]}
resource={MyApp.Worker}
actor={@current_user}
search={[:name]}
option_label={:name}
multiple
max_selections={5}
/>Use the :selected slot and max_visible for richer selected-item UI such
as an avatar stack with a +N overflow token.
Flicker.search/1 has no selection semantics. It parses the input, renders
committed filters as removable pills, and sends the query plus its composed
Ash filter to your LiveView.
<Flicker.search
id="artist-search"
resource={MyApp.Artist}
actor={@current_user}
facets={[
{:status, value_colors: %{active: "#16a34a"}},
:genre,
after: [attribute: :formed_on, op: :>=]
]}
on_change={:artist_query_changed}
/>def handle_info({:artist_query_changed, _query, filter}, socket) do
artists =
MyApp.Artist
|> Ash.Query.filter_input(filter)
|> Ash.read!(actor: socket.assigns.current_user)
{:noreply, assign(socket, :artists, artists)}
endDistinct facet keys are ANDed; repeated values of the same facet are ORed. Unknown keys and malformed values degrade to free text instead of breaking the input. Read the faceted-search guide for the grammar and type-derived behavior.
<Flicker.palette
id="cmdk"
source={MyApp.Search.Global}
actor={@current_user}
open={@palette_open}
on_close={:palette_closed}
on_select={:palette_selected}
/>The palette opens with Cmd+K on macOS or Ctrl+K elsewhere. Results carrying
a group render under group headings; a result with meta.href navigates
with push_navigate/2 after selection. See the
command-palette guide.
Implement two callbacks to use a non-Ash or federated backend:
defmodule MyApp.Search.Global do
@behaviour Flicker.Provider
@impl true
def search(%Flicker.Query{text: text}, opts) do
MyApp.SearchIndex.search(text,
actor: opts[:actor],
tenant: opts[:tenant],
limit: opts[:limit]
)
end
@impl true
def fetch(values, opts) do
MyApp.SearchIndex.fetch_many(values,
actor: opts[:actor],
tenant: opts[:tenant]
)
end
endBoth callbacks return {:ok, [%Flicker.Result{}]} or {:error, reason}.
Optional facets/0 and render_option/2 callbacks extend the surface. The
provider guide documents the complete contract,
pagination offset, and reference static provider.
Flicker uses a class-per-part %Flicker.Theme{} rather than owning your CSS
framework:
config :flicker, default_theme: Flicker.Theme.tailwind()<Flicker.select
theme={%{search_input: "my-input", option_active: "my-active-option"}}
...
/>Choose Flicker.Theme.vanilla/0, tailwind/0, or daisy_ui/0; replace a
whole theme or merge only the parts you need. The
theming guide contains the full part map.
Every visible string and live announcement goes through
Flicker.Messages. Override the module globally or per component for
localization and product voice without forking component markup.
Flicker implements WAI-ARIA editable-combobox behavior, keeps DOM focus on
the input while moving aria-activedescendant, announces loading/results/
selection/error changes, supports the full keyboard map, and traps/restores
focus in the command palette.
CI exercises axe-core against every playground route and drives the client-side keyboard behavior through Chrome. The remaining pre-1.0 accessibility work is the manual VoiceOver, NVDA, and JAWS matrix tracked in Spec 007. Read the accessibility statement and manual test script.
Flicker’s API reference and user guides are built with ExDoc and are ready for HexDocs. Until the first Hex release, generate the same documentation locally:
mix deps.get
mix docsOpen doc/index.html to browse the complete guide set, grouped public API,
providers, integrations, test helpers, and source links. Hex publishes this
documentation automatically with the package; the canonical address will be
https://hexdocs.pm/flicker.
The repository includes a database-free Phoenix app backed by seeded Ash ETS resources:
mix deps.get
mix devVisit http://localhost:4000 for live, copyable examples of the selection
modes, custom slots, facets, pagination, keyboard activation, palette,
themes, providers, Cinder integration, localization, and error states.
Tool versions are pinned in .tool-versions. The main
quality gate covers formatting, Credo, documentation coverage, Sobelow,
dependency audits, Dialyzer, and ExUnit:
mix deps.get
mix checkThe real-browser suite is separate because it requires Chrome and
chromedriver:
mix test --only browserArchitecture and sequencing live in the design document; feature contracts live in specs; structural decisions live in ADRs. Contributions should follow AGENTS.md.
Flicker is released under the Apache License 2.0.
