Switch

v0.0.10

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

The Mob Switch is an on/off control for iOS and Android apps built with Mob. It is part of Mishka Chelekom and draws the platform's own switch, a Jetpack Compose Switch on Android and a SwiftUI Toggle on iOS. It is the native port of the headless Switch for Phoenix LiveView.

Overview of the Mob Switch component for iOS and Android

MishkaSwitch maps the Chelekom switch API onto Mob's native Toggle widget. The switch animates, reports its state to accessibility services and uses the platform's own switch size.

The screen owns the state. On Android the Toggle renders whatever checked says and reports a change through on_change, so nothing moves until your screen writes the new value back. The value arrives in handle_info/2 as {:change, tag, boolean}.

When you set label, the component builds a row with the label first and the switch at the trailing edge. The component builds this row itself, so the label shows on both platforms.

Generate the switch component

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

bash
mix mishka.ui.gen.mob switch

The generator registers the tag, so you can use it in ~MOB markup. It also copies the shared Event module. The examples on this page use the mishka_ prefix of the showcase app (--module-prefix mishka_), so the tag is <MishkaSwitch>. Without a prefix, the tag is <Switch>. To generate with the prefix, run:

bash
mix mishka.ui.gen.mob switch --module-prefix mishka_

Switch with a label

Set label to put text before the switch. The switch sits at the trailing edge of the row. When the user taps it, the screen receives {:change, tag, boolean} and assigns the value. In the showcase app, two switches share one card and a line under them reports both states.

Switch with a label

With a label card: two switch rows, Wi-Fi switched on and Bluetooth switched off, then a divider and the gray text Wi-Fi is on, Bluetooth is off
Two switches labelled Wi-Fi and Bluetooth, both turned on, with the line Wi-Fi is on, Bluetooth is on below.

Left: the example as it loads. Right: after a tap. Flip a switch and the screen state follows.

elixir
<MishkaSwitch label="Wi-Fi" checked={@wifi} on_change={:wifi_changed} />

def handle_info({:change, :wifi_changed, on?}, socket) do
  {:noreply, assign(socket, :wifi, on?)}
end

Switch with custom colors

color tints the thumb and track_color tints the track, so you set the two separately. Pass track_color as an ARGB integer. Both props apply on Android only, because iOS paints the system accent.

Switch with custom colors

Custom colour card: one switch row labeled Brand violet, switched on, with a pale yellow thumb on a violet track, in the MishkaMob showcase app on Android
heex
<MishkaSwitch
  label="Violet"
  checked={@on}
  color={0xFFFDE68A}
  track_color={0xFF7C3AED}
  on_change={:tint}
/>

Disabled switch

disabled leaves the handler off, so the switch cannot report a change. The switch still looks enabled, because Mob's Toggle does not pass the disabled state to the platform widget. On Android it cannot move. On iOS the native switch still moves when touched and reports nothing. Add a muted label if the difference needs to show.

Disabled switch

Disabled card: two switch rows, Locked on with its switch on in a dark track and Locked off with its switch off, in the MishkaMob showcase app on Android
heex
<MishkaSwitch label="Locked on" checked={true} disabled={true} />

Switch without a label

The built-in label always comes first and the switch always comes last. To put the switch first, leave label out and place the switch in a Row beside a Text.

This example routes the change through handle_change/3, the callback the showcase app uses. In your own screen, handle {:change, :airplane, on?} in handle_info/2, as in the first example.

Switch without a label

Without a label card: a switch turned off followed by the text Airplane mode, with a gray note below saying the switch leads here
elixir
# Leading switch, which the label prop cannot produce:
<Row>
  <MishkaSwitch checked={@on} on_change={:airplane} />
  <Text text="Airplane mode" />
</Row>

def handle_change(:airplane, on?, socket),
  do: Mob.Socket.assign(socket, :airplane, on?)

Draw the switch with render box

The native Toggle has fixed metrics. Material's Compose Switch is 52 dp wide and 32 dp tall, with a 24 dp thumb when on, and no prop at any layer of the stack changes that. When a design specifies its own track and thumb sizes, set render={:box}. The component then draws the switch from a track Box that holds a thumb Box.

heex
<MishkaSwitch
  render={:box}
  checked={@wifi?}
  on_toggle={:wifi_tapped}
  track_width={46}
  track_height={28}
  track_radius={14}
  thumb_size={22}
/>

A drawn switch reports a tap through on_toggle, with no value, so your screen flips checked itself. on_change is not read in this mode. disabled still removes the handler, and a drawn switch without a handler is inert on both platforms. color and track_color become backgrounds, so they work on iOS too.

The native switch stays the default, so existing switches look the same. Choose the drawn mode only when you need exact geometry, because it gives up some things the native switch has:

  • No ripple or press state. The track paints the same before and after a touch.
  • No thumb animation. The thumb snaps between its two positions instead of sliding.
  • No platform accessibility semantics. A drawn switch is announced as an unlabeled tappable region, so set label (the label row is real text) and prefer the native switch where accessibility matters more than geometry.
  • One thumb size. The native thumb grows from 16 dp to 24 dp when on, while the drawn thumb is thumb_size in both states.

Props

PropTypeDefaultDescription
checkedbooleanfalseThe on state. The screen owns it and passes it back on every render.
labelstringnilLeading label. The switch sits at the trailing edge.
on_changeevent tagNoneSent as {:change, tag, boolean}. Leave it out for a read-only switch. The native rendering reads it.
colorcolor / ARGBplatform defaultThumb color when on. Android only, because iOS paints the system accent.
track_colorARGB integerplatform defaultTrack color when on. Android only. Color tokens are ignored here, so pass an ARGB integer.
disabledbooleanfalseWires no handler. The switch cannot move on Android. On iOS it moves but reports nothing.
render:toggle or :box:toggle:toggle draws the native Toggle. :box draws a track Box that holds a thumb Box.

Props for render box

The drawn rendering reads these props. The native rendering ignores them.

PropTypeDefaultDescription
track_widthnumber52Track width in dp or pt.
track_heightnumber32Track height in dp or pt.
track_radiusradius token or numbertrack_height / 2Track rounding. The default is a pill.
thumb_sizenumber24Thumb edge, the same in both states.
thumb_radiusradius token or numberthumb_size / 2Thumb rounding. The default is a circle.
thumb_insetnumber(track_height - thumb_size) / 2Gap between the thumb and the leading or trailing edge of the track. The default is a symmetric capsule.
track_on_colorcolor token or ARGB integertrack_color, else :primaryTrack color when on.
track_off_colorcolor token or ARGB integer:borderTrack color when off.
thumb_on_colorcolor token or ARGB integercolor, else :on_primaryThumb color when on.
thumb_off_colorcolor token or ARGB integer:on_primaryThumb color when off. :on_primary is white in the default theme.
disabled_track_colorcolor token or ARGB integerthe enabled track colorTrack color while disabled. The default keeps a disabled switch looking live, so set it to gray the switch.
disabled_thumb_colorcolor token or ARGB integerthe enabled thumb colorThumb color while disabled. Set it to gray the thumb.
thumb_shadowshadow stringnilA string in the form dx dy blur spread #AARRGGBB. It brings back the elevation of Material's handle.
on_toggleevent tagNoneSent as {:tap, tag}.

Events

The native switch sends {:change, tag, boolean} to the screen through on_change. Handle it in handle_info/2 and assign the value:

elixir
def handle_info({:change, :wifi_changed, on?}, socket) do
  {:noreply, Mob.Socket.assign(socket, :wifi?, on?)}
end

A drawn switch (render={:box}) sends {:tap, tag} through on_toggle. The message has no value, so the screen flips the state:

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

Differences from the web component

  • name, value, unchecked_value, form and required are not ported. They exist to submit an <input> inside an HTML form, and a phone has no form post.
  • id is not ported. On the web it anchors aria-* relationships in the DOM.
  • readonly and disabled become one prop. Both omit the handler.