Chip
v0.0.10
mix mishka.ui.gen.headless chip
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)
<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)
<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.
<.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>def handle_event("filter", params, socket) do
topics = get_in(params, ["filters", "topics"]) || []
{:noreply, assign(socket, :topics, topics)}
endHere 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.
| Part | data-part | State / ARIA | Purpose |
|---|---|---|---|
| Root | root | data-disabled | The label that wraps the input and the text. It is the click target for the whole chip. |
| Input | input | checked, disabled | The native checkbox or radio. Hide it visually, but keep it focusable. |
| Label | label | - | 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.
Attributes
| Name | Type | Default | Values | Required | Description |
|---|---|---|---|---|---|
id
|
string
| — | — | No | Optional id for the input |
name
|
string
| — | — | No | Form field name (share it across radio chips) |
value
|
string
| — | — | No | Submitted value when checked |
type
|
string
|
checkbox
|
checkboxradio
| No | Underlying input type |
checked
|
boolean
|
false
| — | No | Whether the chip is selected |
disabled
|
boolean
|
false
| — | No | Disable the chip (also sets data-disabled) |
class
|
any
| — | — | No | Extra classes for the root label |
input_class
|
any
| — | — | No | Extra classes for the hidden input |
label_class
|
any
| — | — | No | Extra classes for the visible label |
rest
|
global
| — | — | No | Any input/global attrs, e.g. phx-change, required |
Slots
| Name | Required | Description |
|---|---|---|
inner_block
| Yes | The chip label |
Options
Each option takes one or more of its values, separated by commas.
| Option | Values |
|---|---|
--type
|
chip
|
--module
| A module name of your own |
What it generates
mix mishka.ui.gen.headless chip