Chip

v0.0.10

v0.0.10StableMobStatelessApache-2.0
Installation mix mishka.ui.gen.mob chip
Source View Source

The Mishka Chelekom Mob Chip is a compact, selectable label for iOS and Android, such as a filter you switch on and off. It is the native port of the headless Chip, built for Mob, which drives Jetpack Compose and SwiftUI from Elixir.

Overview of the Mob Chip component for iOS and Android

On the web a chip wraps a hidden checkbox or radio input, and the browser handles selection and form submission. A Mob chip has no hidden input and no form post. It is a tappable pill whose checked state comes from your screen, like every other controlled Mob widget. The screen owns the state, and a tap arrives in handle_info/2 as {:tap, tag}.

That is also why type is not a prop. A set of checkbox chips and a set of radio chips differ only in what your handler does with the tapped id. Both are one line:

elixir
# checkbox chips: toggle membership
selected = if id in selected, do: List.delete(selected, id), else: [id | selected]

# radio chips: replace
selected = [id]

A disabled chip keeps its checked state. A locked-on chip is muted but still reads as selected, so a user can tell it apart from a locked-off chip.

The event handlers in the examples below are handle/2 clauses, the callback that the showcase pages use. In your own screen they are handle_info/2 clauses, as the Events section shows.

Generate the chip component

Run this from your Mob app's root to add the component to lib/<app>/components/:

bash
mix mishka.ui.gen.mob chip

The generator registers the component's tag, so you can use it in ~MOB markup. It also copies the shared Event module that the component uses. The examples on this page use the mishka_ prefix of the showcase app (--module-prefix mishka_), so the tag is <MishkaChip>. Without a prefix the tag is <Chip>.

Multi-select chips

Checkbox chips let the user pick several values. Each chip checks whether its id is in the list of selected tags, and the handler adds the id when it is missing or removes it when it is there. The tag you give on_toggle carries the chip's id, so one clause handles every chip. Use a tuple such as {:tag, id}. A bare atom does not say which chip was tapped. The card in the screenshot also shows a line that lists the selected tags.

Multi-select chips

Card titled Multi-select, with three chips, Elixir filled dark as selected and Erlang and Gleam white, above the text Selected: elixir, in the MishkaMob showcase app on Android
Three chips with Elixir and Gleam filled and Erlang plain, and the line Selected: elixir, gleam below.

Left: the example as it loads. Right: after a tap. Checkbox chips toggle independently.

elixir
<MishkaChip label="Elixir" checked={:elixir in @tags} on_toggle={{:tag, :elixir}} />

# The tag is COMPOSED with the chip's id, so one clause serves every chip.
# Note the shape: {:tag, id}, not a bare :tag — a bare atom would give you
# no way to tell which chip was tapped.
def handle({:tag, id}, socket) do
  tags = socket.assigns.tags

  next = if id in tags, do: List.delete(tags, id), else: [id | tags]
  Mob.Socket.assign(socket, :tags, next)
end

Single-select chips

Radio chips let the user pick one value. The handler replaces the selection instead of toggling it, so there is no way to clear it. That is what makes the set behave as a radio group and not as a checkbox set. The code shows only the handler. The screen builds three chips and sets checked on the one that matches the selection.

Single-select chips

Card titled Single select, with three chips in a row, Low, Normal and High, where Normal is filled dark as the selected one, in the MishkaMob showcase app on Android
Three chips labelled Low, Normal and High with High filled.

Left: the example as it loads. Right: after a tap. Radio chips replace the selection.

elixir
# Single select replaces rather than toggles — there is no way to clear it,
# which is exactly what makes it a radio group and not a checkbox set.
def handle({:size, id}, socket),
  do: Mob.Socket.assign(socket, :size, id)

Disabled chip

disabled mutes the chip and wires no handler, whether or not it is checked. The card in the screenshot shows a locked chip that is on next to one that is off.

Disabled chip

Card titled Disabled, with two chips side by side: Locked on filled muted gray and Locked off in pale muted text on white, in the MishkaMob showcase app on Android
heex
<MishkaChip label="Locked" checked={true} disabled={true} />

Chip color

color fills the chip when it is checked, and text_color colors its label. Both take a theme token or an ARGB integer. The card in the screenshot also shows a red chip and a chip in the default colors.

Chip color

Card titled Colour, with three selected chips in white text: Violet on violet, Danger on red and Default on near black, in the MishkaMob showcase app on Android
heex
<MishkaChip label="Violet" checked={true} color={0xFF7C3AED} text_color={0xFFFFFFFF} />

Size and trailing content of a chip

The props after text_color in the Props table size and style a chip to a design. Each is optional, and an unset prop leaves the chip as a box that holds one label. Props that take a size accept a theme token or a raw value, so padding_x={:space_md} and padding_x={15} are both valid.

The bridge applies padding before width and height, so a declared size is the size of the content box and not of the chip. With the default :space_sm padding, height={32} measures 48. To pin a design height, pin the padding on that axis too. The axis you leave alone stays on padding.

heex
<MishkaChip label="Unread" trailing="12" trailing_gap={6} height={32} padding_y={0} />

A chip with a width or a height centers its label unless you set align. A chip with neither sends no align.

trailing is a slot. A string becomes a text in the chip's own color and size, and a node or a list of nodes is placed as you give it. The slot sits in a row beside the label, which suits a count after a filter name. The composite tag ignores its children, so trailing is how extra content gets in.

Props

PropTypeDefaultDescription
labelstringnilThe chip's text.
checkedbooleanfalseWhether the chip reads as selected. The screen owns this state.
disabledbooleanfalseWires no handler and mutes the chip.
on_toggleevent tagNoneSent to the screen as {:tap, tag}.
colorcolor token or ARGB integer:primaryFill when checked.
text_colorcolor token or ARGB integer:on_primaryLabel color when checked.
unchecked_colorcolor token or ARGB integer:surface_raisedFill when unchecked.
unchecked_text_colorcolor token or ARGB integer:on_surfaceLabel color when unchecked.
disabled_colorcolor token or ARGB integer:muted when checked, otherwise unchecked_colorFill while disabled. Setting it gives both disabled states the same look.
disabled_text_colorcolor token or ARGB integertext_color when checked, otherwise :mutedLabel color while disabled.
widthnumber (dp)Not set, the chip hugs its labelDeclared width.
heightnumber (dp)Not set, intrinsic heightDeclared height.
paddingspacing token or number:space_smPadding on all four sides.
padding_xspacing token or numberpaddingLeft and right padding. Overrides padding.
padding_yspacing token or numberpaddingTop and bottom padding. Overrides padding.
corner_radiusradius token or number:radius_pillCorner rounding.
text_sizetext size token or number:baseLabel size.
font_weight:medium, :semibold, :bold, :light or :thinNot set, regularLabel weight.
max_linesintegerNot set, unboundedCaps the label at this many lines.
align:center, :leading, :trailing and other alignmentsNot set, :center when the chip has a width or heightWhere the content sits in the box.
trailingstring, node or list of nodesnilContent after the label, such as a count, a ✕ or an icon.
trailing_gapnumber (dp)0Space between the label and trailing.

Events

Your screen receives {:tap, tag} in handle_info/2 when the user taps the chip. A disabled chip sends nothing. Give every chip a tuple tag that holds its id, so one clause handles all of them:

elixir
def handle_info({:tap, {:tag, id}}, socket) do
  tags = socket.assigns.tags

  next = if id in tags, do: List.delete(tags, id), else: [id | tags]
  {:noreply, Mob.Socket.assign(socket, :tags, next)}
end

For a single-select set, replace the selection in the same clause instead of toggling it.

Differences from the web component

These attributes of the web component are not ported:

  • type. Checkbox and radio behavior lives in your handler, as the overview shows.
  • name and value. They are HTML form plumbing, and a native chip posts no form.
  • id and the *_class attributes.