Tags Input

v0.0.10

v0.0.10StableHeadlessStatelessAccessibleJS-moduleApache-2.0
Installation mix mishka.ui.gen.headless tags_input
Source View Source

Overview of the Phoenix and LiveView Headless Tags Input Component

The Mishka Chelekom Headless Tags Input is a control for entering a list of short values in Phoenix and LiveView, with no styling of its own. It shows removable tags and a text field for adding more. It is stateless: you own the tags list. Clicking anywhere in the control focuses the text field through JS.focus, so it needs no JavaScript hook.

To add a tag, wrap the control in a form with phx-submit, so Enter submits the form and its reset clears the draft, or pass on_add, a phx-keydown handler that fires on Enter. To remove one, pass on_remove, a phx-click value such as JS.push("remove"). The click carries the tag in phx-value-tag. With name set, including the brackets as in user[tags][], every tag renders a hidden input, so the list submits with the surrounding form. The parts take class, tag_class, tag_label_class, remove_class and input_class. id is required.

Base UI Tags Input example - tags and a draft field (Phoenix headless)

Base UI Tags Input example - tags and a draft field (Phoenix headless)

Design Engineering Product
heex
<.tags_input
  id="baseui-tags-input"
  tags={["Design", "Engineering", "Product"]}
  placeholder="Add a tag…"
  class="flex w-full max-w-80 flex-wrap items-center gap-1.5 rounded-lg border border-neutral-300 bg-white px-2 py-1.5 text-sm cursor-text focus-within:ring-2 focus-within:ring-neutral-400 dark:border-neutral-700 dark:bg-neutral-900"
  tag_class="inline-flex items-center gap-1 rounded bg-neutral-100 py-0.5 pr-1 pl-2 dark:bg-neutral-800"
  remove_class="inline-flex size-4 items-center justify-center rounded-full text-neutral-500 hover:bg-neutral-200 dark:hover:bg-neutral-700"
  input_class="min-w-24 flex-1 border-0 bg-transparent p-0.5 outline-none"
/>

Adding and removing tags

Keep the tags in the LiveView, add them from the form's submit event and remove them from the click event. input_name and input_value bind the draft field to the form, so resetting the form clears it.

heex
<.form for={@form} id="tags-form" phx-submit="add">
  <.tags_input
    id="tags"
    name="tags_form[tags][]"
    tags={@tags}
    input_name={@form[:tag].name}
    input_value={@form[:tag].value}
    placeholder="Add a tag and press Enter…"
    on_remove={JS.push("remove")}
    class="flex w-full max-w-80 flex-wrap items-center gap-1.5 rounded-lg border border-neutral-300 bg-white px-2 py-1.5 text-sm cursor-text focus-within:ring-2 focus-within:ring-neutral-400 dark:border-neutral-700 dark:bg-neutral-900"
    tag_class="inline-flex items-center gap-1 rounded bg-neutral-100 py-0.5 pr-1 pl-2 dark:bg-neutral-800"
    remove_class="inline-flex size-4 items-center justify-center rounded-full text-neutral-500 hover:bg-neutral-200 dark:hover:bg-neutral-700"
    input_class="min-w-24 flex-1 border-0 bg-transparent p-0.5 outline-none"
  />
</.form>
elixir
def mount(_params, _session, socket) do
  {:ok, assign(socket, tags: ["elixir", "phoenix"], form: to_form(%{"tag" => ""}, as: :tags_form))}
end

def handle_event("add", %{"tags_form" => %{"tag" => tag}}, socket) do
  tag = String.trim(tag)
  tags = socket.assigns.tags
  tags = if tag != "" and tag not in tags, do: tags ++ [tag], else: tags

  {:noreply, assign(socket, tags: tags, form: to_form(%{"tag" => ""}, as: :tags_form))}
end

def handle_event("remove", %{"tag" => tag}, socket) do
  {:noreply, assign(socket, tags: List.delete(socket.assigns.tags, tag))}
end

For a demo with no state, on_remove can hide the tag on the client instead of pushing an event: on_remove={JS.hide(to: {:closest, "[data-part=tag]"})}. The list in your LiveView does not change.

Without a wrapping form, pass on_add instead. The handler fires on Enter and receives the key event with the field's value.

heex
<.tags_input
  id="keywords"
  tags={@keywords}
  on_add="add_keyword"
  on_remove={JS.push("remove_keyword")}
  class="..."
/>

Parts & data attributes

The root carries chelekom-tags-input and data-disabled. Each part has a data-part value and a chelekom-tags-input__* class.

  • tag is one token. It holds its label, a remove button and, when name is set, a hidden input with the tag.
  • tag-label is the text of the tag.
  • remove is a button labelled "Remove" plus the tag. It fires on_remove.
  • input is the draft field, with the id <id>-input.

disabled sets data-disabled on the root, disables the field and the remove buttons, and turns off click-to-focus.

Accessibility

A tags input has no formal WAI-ARIA pattern. It is a text field plus removable tokens. Enter adds the draft, and each remove button is a real button with its own label, so keyboard and screen reader users can remove a tag. Give the field a visible label. For suggestions, combine it with the headless Autocomplete.