Toggle

v0.0.10

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

Mob Toggle is a button that stays pressed, like bold or italic in a formatting toolbar, for iOS and Android. It comes from Mishka Chelekom and runs on Mob, which renders native Jetpack Compose and SwiftUI views from Elixir. It is the native port of the headless Toggle.

Overview of the Mob Toggle component for iOS and Android

Mob has three controls that mean on or off, and the port keeps them apart on purpose. Switch is a setting that wraps the platform's switch widget, and it belongs beside a label in a settings list. Chip is a filter, one of a set you pick from, and it is pill-shaped. Toggle is a pressed button, usually one of several in a toolbar, that holds a binary state about the thing you are editing and not about the app.

They look different so users can tell them apart. A switch slides, a chip is a pill, and a toggle is a square-cornered button that looks pushed in.

The screen owns the state. Pass pressed from the screen, and flip it when {:tap, tag} arrives. A lone toggle owns one boolean, so its tag needs no id.

Toggle is a box around a text label, so every dimension it draws is a prop. If a design wants a 46 by 28 pressed button, you set that size and get it. Switch cannot be resized this way, because it wraps a native widget with a fixed size.

Styling

As with the headless component, Toggle ships no look. There is no stylesheet on a phone, so every visual decision is a prop. The defaults only keep an unstyled toggle legible. A pressed toggle uses color and text_color, and an idle one uses background and label_color.

fill_width defaults to false, so a toggle is as wide as its label and a row of toggles is possible. Set it to true for a toggle that should span its container.

Size and padding

Padding is applied before width and height, so those measure the box inside the padding. height: 28, padding: 8 is 44 tall on screen. Pin the outer size by pairing the two, or drop the padding on the axis you pin. width and height take plain numbers, not tokens. Never compute a negative padding, because it throws on the native side.

elixir
toggle(label: "W", width: 46, height: 28, padding: 0, border_width: 0,
       corner_radius: 8, text_size: 11.5, font_weight: :semibold)

text_size also takes a raw number in sp. Only the token form is multiplied by the theme's type scale, so a number stays put when the user scales their type.

Shadows

shadow is a string in CSS box-shadow order, dx dy blur spread #AARRGGBB, with further layers after a vertical bar. There is one prop and not a pressed and idle pair, because the screen already knows which state it asked for and can pass the matching shadow. A bridge without shadow support ignores it.

Generate the toggle component

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

bash
mix mishka.ui.gen.mob toggle

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

Toggles in a formatting toolbar

Each button holds its own pressed state, and the screen keeps one boolean per toggle. The code shows one toggle and the handler that flips it. The app draws Bold, Italic and Underline side by side.

Toggles in a formatting toolbar

A toolbar card: three toggle buttons, Bold pressed in black and Italic and Underline outlined, with the gray text Pressed: bold below them, in the MishkaMob showcase app on Android
A row of toggle buttons Bold and Italic pressed and Underline plain, with the line Pressed: bold, italic.

Left: the example as it loads. Right: after a tap. Each toggle holds its own pressed state.

elixir
<MishkaToggle label="Bold" pressed={@bold?} on_change={:bold} />

# A lone toggle owns one boolean, so its tag needs no id.
def handle_info({:tap, :bold}, socket) do
  {:noreply, Mob.Socket.assign(socket, :bold?, not socket.assigns.bold?)}
end

def handle_info(_msg, socket), do: {:noreply, socket}

A toggle hugs its label

A toggle is as wide as its text, because it is a button and not a banner. Add fill_width={true} when you want one to span the parent.

A toggle hugs its label

It hugs its label card: a pressed toggle labeled Pressed beside an outlined toggle labeled A much longer label, then a full width black toggle labeled Full width
heex
<MishkaToggle label="Pressed" pressed={true} />
<MishkaToggle label="A much longer label" />

Style a toggle with props

The toggle ships no look, so every visual is a prop. This one is pressed in violet, with a corner radius of 20, padding of 12 and no border.

Style a toggle with props

Styled by props, not by a stylesheet card: two Pill toggles, one pressed in violet and one plain, and two Square toggles, one pressed in black and one plain
heex
<MishkaToggle
  label="Violet"
  pressed={true}
  color={0xFF7C3AED}
  corner_radius={20}
  padding={12}
  border_width={0}
/>

Disabled toggle

A disabled toggle is inert, but it still shows whether it is pressed. A pressed one stays filled and turns gray, so locked on and locked off do not look the same.

Disabled toggle

Disabled card: two toggles, Locked on filled gray and Locked off outlined, with a gray note below explaining that a disabled toggle stays filled
heex
<MishkaToggle label="Locked on" pressed={true} disabled={true} />

Props

PropTypeDefaultDescription
labelstringnilButton text. Children override it.
pressedbooleanfalseWhether it reads as pushed in. The screen owns it.
disabledbooleanfalseWires no handler and mutes the toggle.
on_changeevent tagNoneSent as {:tap, tag}.
colorcolor token or ARGB integer:primaryFill when pressed.
text_colorcolor token or ARGB integer:on_primaryLabel color when pressed.
backgroundcolor token or ARGB integer:surface_raisedFill when idle.
label_colorcolor token or ARGB integer:on_surfaceLabel color when idle.
paddingspacing token or number:space_smSpace inside the button.
padding_top, padding_right, padding_bottom, padding_leftspacing token or numberunsetOne edge, overriding padding on that side only.
corner_radiusradius token or number:radius_mdCorner rounding.
border_colorcolor token or ARGB integer:borderBorder color.
border_widthnumber10 removes the border.
text_sizetext token or number:baseLabel size. A number is in sp.
font_weight:regular, :medium, :semibold, :bold, :light or :thinunset (regular)Label weight.
letter_spacingnumber (sp)unsetTracking on the label.
line_heightnumberunsetA multiplier of text_size. Ignored without one.
max_linesnumberunset (unlimited)Caps the label and ellipsizes the rest.
widthnumberunsetExact width. Unset, the toggle hugs its label.
heightnumberunsetExact height. Unset, the toggle hugs its label.
fill_widthbooleanfalseSpan the parent instead of hugging the label.
align:center, :leading, :trailing and others:centerWhere the label sits inside the button.
shadowshadow stringunsetPassed to the root box, as dx dy blur spread #AARRGGBB.
idstringnilTest tag, suffixed -pressed or -idle.

A pressed toggle differs from an idle one by its fill color alone, and color is not in the accessibility tree. The id suffix is how a device test reads the state.

Events

on_change sends {:tap, tag} to the screen. Flip the boolean in handle_info/2:

elixir
def handle_info({:tap, :bold}, socket) do
  {:noreply, Mob.Socket.assign(socket, :bold?, not socket.assigns.bold?)}
end

Differences from the web component

  • name, value, unchecked_value and form are not ported, because they are HTML form plumbing.
  • The *_class attributes are not ported. Every visual is a prop instead.