Floating indicator

v0.0.10

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

The Mob Floating Indicator in Mishka Chelekom marks one active target among several on iOS and Android screens built with Mob, in a horizontal row or a vertical stack. It is the native port of the headless Floating Indicator.

Overview of the Mob Floating Indicator component for iOS and Android

The web version measures the active target with a ResizeObserver and slides one absolutely positioned box over it. Mob reports no geometry back to render/1 and has no animation primitive, so neither half is available.

What the indicator is for is still available: exactly one target is highlighted, and it is the one that matches active. The Mob version draws that highlight as the active target's own background instead of a box flying over it. No measurement is needed, and there is still one highlight, never two. It does not slide.

Targets are maps with a label and a value. The active target gets the color background and a semibold label. Other targets are transparent with a muted, regular label. A target with disabled: true is muted and sends no event. The disabled prop does the same for the whole group.

The screen owns active. A tap on a target sends {:tap, {tag, value}}, and the screen assigns the new value. active?/2 is public so you can style anything else with the same rule: it returns true only when value equals a non-nil active.

Layout

In a row the targets split the group equally. Each one carries weight: 1, which is the row's own way to share its width. In a stack the targets keep their natural height and fill the width of the column, because a weight on a column's main axis would stretch them instead.

Generate the floating indicator component

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

bash
mix mishka.ui.gen.mob floating_indicator

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

Floating indicator with three targets

The active target draws its own highlight, so the group needs no measurement and exactly one target is highlighted.

A tap on a target sends {:tap, {:pick, value}}, and the handler assigns the new active value.

Floating indicator with three targets

Floating indicator example card with a strip of three targets, Day raised in a white pill, then Week and Month, and the text Showing: day below it
A three option strip Day, Week and Month with Week highlighted, and the line Showing: week below.

Left: the example as it loads. Right: after a tap. The active target draws its own highlight.

elixir
<MishkaFloatingIndicator targets={@tabs} active={@tab} on_change={:pick} />

def handle_info({:tap, {:pick, value}}, socket), do: {:noreply, assign(socket, :tab, value)}

Vertical floating indicator

Set orientation={:vertical} to stack the targets in a column instead of a row. A target with disabled: true stays muted and does not respond.

Vertical floating indicator

Stacked indicator example card with a vertical list of Day in a raised white pill, then Week and Archived in muted gray text, in the MishkaMob showcase app on Android
heex
<MishkaFloatingIndicator targets={@targets} active={@tab} orientation={:vertical} />

Props

PropTypeDefaultDescription
targetslist of maps with label and value[]The choices. A map may also set disabled: true.
activeany valuenilWhich target is highlighted. Exactly one is.
orientation:horizontal or :vertical:horizontalRow or stack. In a row the targets share the width equally.
colorcolor token or ARGB integer:surface_raisedThe highlight.
disabledbooleanfalseMutes every target and unwires them.
on_changeevent tagnoneSent as {:tap, {tag, value}} when a user taps a target.

active?/2

active?(value, active) returns true when value equals active and active is not nil. It is the component's whole semantics.

Events

Your screen receives {:tap, {tag, value}} in handle_info/2 when a user taps an enabled target. The tag is the one you set in on_change, and value is the target's own.

elixir
def handle_info({:tap, {:pick, value}}, socket) do
  {:noreply, Mob.Socket.assign(socket, :tab, value)}
end

Differences from the web component

The Mob Floating Indicator keeps the web component's rule of one highlight, with two changes:

  • It does not measure the active target, because Mob reports no geometry back to render/1.
  • It does not slide, because Mob has no animation primitive.