Chip

v0.0.10

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

Overview of the Phoenix and LiveView Headless Chip Component

The Mishka Chelekom Headless Chip is a selectable pill for Phoenix and LiveView, built on a real input. It renders a <label> that wraps a native <input type="checkbox"> (or type="radio") and a <span> with the visible text. Selection, Space to toggle and form submission are native browser behavior, so a chip needs no JavaScript and works with phx-change like any other input.

The component ships no checked look. Hide the input and style the label with the CSS :has(:checked) selector, as the examples do. The label takes class, the input takes input_class and the visible text takes label_class. A disabled chip sets data-disabled on the label. Extra attributes such as required, form, phx-change, aria-* and data-* go on the input.

Base UI Chip example - multi-select filter chips (Phoenix headless)

Checkboxes that share a name ending in [] are submitted as a list. The React chip starts checked.

Base UI Chip example - multi-select filter chips (Phoenix headless)

heex
<div class="flex flex-wrap gap-2">
  <.chip
    id="baseui-chip-react"
    name="stack[]"
    value="react"
    checked
    class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-neutral-300 px-3 py-1 text-sm text-neutral-800 select-none has-[:checked]:border-neutral-900 has-[:checked]:bg-neutral-900 has-[:checked]:text-white has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-neutral-400 dark:border-neutral-700 dark:text-neutral-200 dark:has-[:checked]:border-white dark:has-[:checked]:bg-white dark:has-[:checked]:text-neutral-900 [&_[data-part=input]]:sr-only"
  >
    React
  </.chip>
  <.chip
    id="baseui-chip-vue"
    name="stack[]"
    value="vue"
    class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-neutral-300 px-3 py-1 text-sm text-neutral-800 select-none has-[:checked]:border-neutral-900 has-[:checked]:bg-neutral-900 has-[:checked]:text-white has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-neutral-400 dark:border-neutral-700 dark:text-neutral-200 dark:has-[:checked]:border-white dark:has-[:checked]:bg-white dark:has-[:checked]:text-neutral-900 [&_[data-part=input]]:sr-only"
  >
    Vue
  </.chip>
  <.chip
    id="baseui-chip-svelte"
    name="stack[]"
    value="svelte"
    class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-neutral-300 px-3 py-1 text-sm text-neutral-800 select-none has-[:checked]:border-neutral-900 has-[:checked]:bg-neutral-900 has-[:checked]:text-white has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-neutral-400 dark:border-neutral-700 dark:text-neutral-200 dark:has-[:checked]:border-white dark:has-[:checked]:bg-white dark:has-[:checked]:text-neutral-900 [&_[data-part=input]]:sr-only"
  >
    Svelte
  </.chip>
</div>

Base UI Chip example - single-select size row (Phoenix headless)

Set type="radio" and give every chip the same name. At most one of them is checked at a time, and the arrow keys move between them.

Base UI Chip example - single-select size row (Phoenix headless)

heex
<div class="flex flex-wrap gap-2">
  <.chip
    id="baseui-chip-size-s"
    type="radio"
    name="size"
    value="s"
    class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-neutral-300 px-3 py-1 text-sm text-neutral-800 select-none has-[:checked]:border-neutral-900 has-[:checked]:bg-neutral-900 has-[:checked]:text-white has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-neutral-400 dark:border-neutral-700 dark:text-neutral-200 dark:has-[:checked]:border-white dark:has-[:checked]:bg-white dark:has-[:checked]:text-neutral-900 [&_[data-part=input]]:sr-only"
  >
    Small
  </.chip>
  <.chip
    id="baseui-chip-size-m"
    type="radio"
    name="size"
    value="m"
    checked
    class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-neutral-300 px-3 py-1 text-sm text-neutral-800 select-none has-[:checked]:border-neutral-900 has-[:checked]:bg-neutral-900 has-[:checked]:text-white has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-neutral-400 dark:border-neutral-700 dark:text-neutral-200 dark:has-[:checked]:border-white dark:has-[:checked]:bg-white dark:has-[:checked]:text-neutral-900 [&_[data-part=input]]:sr-only"
  >
    Medium
  </.chip>
  <.chip
    id="baseui-chip-size-l"
    type="radio"
    name="size"
    value="l"
    class="inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-neutral-300 px-3 py-1 text-sm text-neutral-800 select-none has-[:checked]:border-neutral-900 has-[:checked]:bg-neutral-900 has-[:checked]:text-white has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-neutral-400 dark:border-neutral-700 dark:text-neutral-200 dark:has-[:checked]:border-white dark:has-[:checked]:bg-white dark:has-[:checked]:text-neutral-900 [&_[data-part=input]]:sr-only"
  >
    Large
  </.chip>
</div>

Chips in a form

The chip is a normal input, so a phx-change on the surrounding form reports the checked values and the server can pass them back through checked. A disabled chip is never submitted. When no chip is checked the browser sends nothing for that name, so default to an empty list.

heex
<.form for={%{}} as={:filters} id="topic-filters" phx-change="filter">
  <div class="flex flex-wrap gap-2">
    <.chip
      :for={topic <- ~w(design elixir phoenix liveview)}
      name="filters[topics][]"
      value={topic}
      checked={topic in @topics}
      class={chip_class()}
    >
      {topic}
    </.chip>
    <.chip name="filters[topics][]" value="locked" disabled class={chip_class()}>
      locked
    </.chip>
  </div>
</.form>
elixir
def handle_event("filter", params, socket) do
  topics = get_in(params, ["filters", "topics"]) || []
  {:noreply, assign(socket, :topics, topics)}
end

Here chip_class/0 is your own function that returns the classes from the examples above, plus data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 for the disabled chip.

Parts & data attributes

Each part carries a stable data-part and a structural chelekom-chip__* class you can target. The checked state is the native :checked pseudo-class of the input.

Partdata-partState / ARIAPurpose
Rootrootdata-disabledThe label that wraps the input and the text. It is the click target for the whole chip.
Inputinputchecked, disabledThe native checkbox or radio. Hide it visually, but keep it focusable.
Labellabel-The span that holds the visible text.

Accessibility

The native input carries the semantics, so a checkbox chip follows the WAI-ARIA Checkbox pattern and a radio chip follows the WAI-ARIA Radio Group pattern. Space toggles a checkbox, and the arrow keys move between radio chips that share a name. Wrap a set of related chips in an element with a name of its own, for example role="group" or role="radiogroup" with an aria-label, so assistive technology announces what the set is for.