Pill

v0.0.10

v0.0.10StableHeadlessStatelessAccessibleApache-2.0
Installation mix mishka.ui.gen.headless pill
Source View Source

Overview of the Phoenix and LiveView Headless Pill Component

The Mishka Chelekom Headless Pill is a compact tag or token. It renders a span that holds your content and, when you set with_remove, a trailing button that removes it. It has no JavaScript and no styling. Use it to show values a person has already chosen, such as the tags on a post or the selections in a multi-select.

The remove button carries an accessible aria-label and calls whatever you give on_remove, so in a Phoenix LiveView it can be a plain event name or a Phoenix.LiveView.JS command. Each part takes its own class attribute: class for the root, then label_class and remove_class.

Base UI Pill example - read-only and removable pills (Phoenix headless)

Base UI Pill example - read-only and removable pills (Phoenix headless)

Design Engineering Product
heex
<div class="flex flex-wrap items-center gap-2">
  <.pill
    id="baseui-pill-design"
    class="inline-flex items-center gap-1 rounded-full bg-neutral-100 px-2.5 py-0.5 text-sm text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200"
  >
    Design
  </.pill>
  <.pill
    id="baseui-pill-eng"
    with_remove
    remove_label="Remove Engineering"
    class="inline-flex items-center gap-1 rounded-full bg-neutral-100 py-0.5 pr-1 pl-2.5 text-sm text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200"
    remove_class="inline-flex size-4 items-center justify-center rounded-full text-neutral-500 hover:bg-neutral-200 dark:hover:bg-neutral-700"
  >
    Engineering
  </.pill>
  <.pill
    id="baseui-pill-product"
    with_remove
    remove_label="Remove Product"
    class="inline-flex items-center gap-1 rounded-full bg-neutral-100 py-0.5 pr-1 pl-2.5 text-sm text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200"
    remove_class="inline-flex size-4 items-center justify-center rounded-full text-neutral-500 hover:bg-neutral-200 dark:hover:bg-neutral-700"
  >
    Product
  </.pill>
</div>

Removing pills and posting the list

on_remove is forwarded to the remove button as phx-click. Give it an event name, or a JS command. The pill does not know which item it stands for, so use JS.push with a value to say which one. Put a hidden input inside the pill and the list posts as an array with the form: removing a pill removes its input too.

heex
<.form for={@form} phx-submit="save">
  <div class="flex flex-wrap items-center gap-1.5">
    <.pill
      :for={tag <- @labels}
      id={"label-#{tag}"}
      with_remove
      remove_label={"Remove #{tag}"}
      on_remove={JS.push("remove", value: %{tag: tag})}
      class="..."
      remove_class="..."
    >
      {tag}
      <input type="hidden" name="labels[]" value={tag} />
    </.pill>
  </div>
</.form>
elixir
def handle_event("remove", %{"tag" => tag}, socket) do
  {:noreply, assign(socket, :labels, List.delete(socket.assigns.labels, tag))}
end

remove_label is the button's aria-label and defaults to "Remove". Make it specific, such as "Remove Engineering", so the buttons can be told apart in a list. disabled sets data-disabled on the root and disables the remove button.

Parts & data attributes

Each part carries a stable data-part and a chelekom-pill__* class you can target.

Partdata-partState / ARIAPurpose
Rootrootdata-disabledThe span that holds the pill.
Labellabel-A span around the content.
Removeremovearia-label, disabledA button with type="button", rendered only when with_remove is set. It shows a multiplication sign that is hidden from assistive technology.

Accessibility

A pill is presentational, and there is no WAI-ARIA pattern for it. The remove control is a native button, so Enter and Space activate it, and its aria-label tells screen reader users what it removes. The multiplication sign inside it is hidden with aria-hidden. Keep each remove_label unique within a list.