Mob.Theme (mob v0.9.26)

Copy Markdown View Source

Design token system for Mob apps.

A theme is a compiled %Mob.Theme{} struct — a flat map of semantic tokens for colors, spacing, radii, and scale factors. The renderer resolves these tokens at render time so every component picks up the active theme automatically.

Using a named theme

Named themes are plain modules that export theme/0. Pass the module to use Mob.App:

use Mob.App, theme: MobThemes.Obsidian   # (the mob_themes style package)

Override individual tokens without leaving the theme:

use Mob.App, theme: {MobThemes.Obsidian, primary: :rose_500}

Anyone can publish a theme as a Hex package — any module with theme/0 returning a Mob.Theme.t() works:

use Mob.App, theme: AcmeCorp.BrandTheme

Building a theme from scratch

Pass a keyword list of overrides against the neutral base:

use Mob.App, theme: [primary: :emerald_500, type_scale: 1.1]

Or change the theme at runtime (e.g. for accessibility or user preference):

Mob.Theme.set(MobThemes.Obsidian)
Mob.Theme.set({MobThemes.Obsidian, type_scale: 1.2})
Mob.Theme.set({MobThemes.Obsidian, text_scale: 1.3})   # the user's text size
Mob.Theme.set(primary: :pink_500)

Base theme

When no theme is set the renderer uses the neutral base — plain dark grays with a standard blue primary. Functional, not opinionated. Good enough for hello world; swap in a named theme when you want personality.

Token reference

Semantic color tokens

:primary        — main action colour          (default :blue_500)
:on_primary     — text/icons on primary        (default :white)
:secondary      — secondary action colour      (default :gray_600)
:on_secondary   — text/icons on secondary      (default :white)
:background     — page/screen background       (default :gray_900)
:on_background  — text on background           (default :gray_100)
:surface        — card / sheet background      (default :gray_800)
:surface_raised — elevated card background     (default :gray_700)
:on_surface     — text/icons on surface        (default :gray_100)
:muted          — secondary/placeholder text   (default :gray_500)
:error          — error state colour           (default :red_500)
:on_error       — text/icons on error          (default :white)
:border         — dividers and outlines        (default :gray_700)

Spacing tokens (scaled by space_scale)

:space_xs  →  4 × scale
:space_sm  →  8 × scale
:space_md  → 16 × scale
:space_lg  → 24 × scale
:space_xl  → 32 × scale

Radius tokens

:radius_sm   → theme.radius_sm   (default  6)
:radius_md   → theme.radius_md   (default 10)
:radius_lg   → theme.radius_lg   (default 16)
:radius_pill → theme.radius_pill (default 100)

Scale factors

type_scale:  1.0  # multiply the text size tokens (:sm, :base, :xl…) by this
text_scale:  1.0  # multiply every text_size / font_size prop, number or token
space_scale: 1.0  # multiply all spacing tokens by this

type_scale belongs to the theme's design: it scales the token sizes and leaves a numeric text_size (a size pinned to a design) alone. text_scale is the user's text-size preference (a Settings choice such as Small … Largest): the renderer multiplies it into every node's text_size and font_size prop, numeric or token, so an app written in exact numbers still grows. Only sizes on the node scale: a :text with no text_size keeps the platform default (give it :base to follow), and a sized :icon's glyph grows with it. Not scaled: a Canvas text op's size (it's in the drawing's coordinates) and sizes nested in a plugin's own props (a style map). It multiplies the OS font scale where native follows one: Android text sizes are sp; on iOS, custom-font text (Font.custom(_:size:)) follows Dynamic Type, while system-font text and the UIKit-backed text field are fixed points. At 1.0 (the default) every value reaches native exactly as written. Set it with the rest of the theme, and keep it when you switch themes:

Mob.Theme.set({MyApp.Theme.Dark, text_scale: 1.3})

Font tokens

fonts is a name → value map, resolved by the renderer exactly like colors (font: :heading walks this map, same two-step shape as text_color: :primary). :default is special — set it and the renderer injects it automatically onto any node that doesn't specify its own font:, so the whole app picks up a custom font without repeating it everywhere:

Mob.Theme.set(
  fonts: %{
    default: Mob.Theme.font("Inter-Regular", from_file: "priv/fonts/Inter-Regular.ttf"),
    heading: Mob.Theme.font("Inter-Bold", from_file: "priv/fonts/Inter-Bold.ttf")
  }
)

# in render/1:
%{type: :text, props: %{text: "Section", font: :heading}, children: []}
%{type: :text, props: %{text: "Body copy"}, children: []}  # gets :default automatically

font_fallback is an ordered list of font names tried, in order, if the resolved font can't be loaded on-device. Empty by default (the platform's own system-font fallback still applies) — set it when you want an explicit intermediate fallback before that.

A font value is either built with Mob.Theme.font/2 (recommended — it computes the Android name from the actual bundled file) or a bare string used as-is on both platforms.

Summary

Functions

Build a theme from a keyword list of overrides against the neutral base.

Returns the current OS appearance: :light or :dark.

Return the currently active theme (or the neutral base if none is set).

Return the neutral base theme.

Builds a font token value: the iOS PostScript name as given, paired with the Android resource name computed from the bundled file via Mob.Font.android_resource_name/1 — the same function mob_dev's asset planner uses when it copies the file into res/font/. One computation, used on both the build side and the theme side, so the two can't drift apart the way a hand-typed Android name could.

Returns the active theme's palette resolved to ARGB integers — semantic tokens (:primary, :on_surface, …) walked through the theme's color map and then through Mob.Renderer.colors/0. Used to push concrete values to the native side (Mob.Theme.set/1 does this automatically; callers usually don't need to invoke this directly).

Set the active theme. Accepts

Types

color_value()

@type color_value() :: atom() | non_neg_integer()

font_spec()

@type font_spec() :: %{optional(:ios) => String.t(), optional(:android) => String.t()}

font_value()

@type font_value() :: font_spec() | String.t()

t()

@type t() :: %Mob.Theme{
  background: term(),
  border: term(),
  error: term(),
  font_fallback: term(),
  fonts: term(),
  glass: term(),
  muted: term(),
  on_background: term(),
  on_error: term(),
  on_primary: term(),
  on_secondary: term(),
  on_surface: term(),
  primary: term(),
  radius_lg: term(),
  radius_md: term(),
  radius_pill: term(),
  radius_sm: term(),
  secondary: term(),
  space_scale: term(),
  surface: term(),
  surface_raised: term(),
  text_scale: term(),
  type_scale: term()
}

Functions

build(overrides \\ [])

@spec build(keyword()) :: t()

Build a theme from a keyword list of overrides against the neutral base.

Mob.Theme.build(primary: :emerald_500, type_scale: 1.1)

color_scheme()

@spec color_scheme() :: :light | :dark

Returns the current OS appearance: :light or :dark.

Reads from the platform NIF (UITraitCollection.userInterfaceStyle on iOS, Configuration.uiMode & UI_MODE_NIGHT_MASK on Android). Falls back to :light when running on the host BEAM (no NIF loaded), on platforms that don't expose appearance, or on legacy Android apps that haven't added MobBridge.getColorScheme() yet.

current()

@spec current() :: t()

Return the currently active theme (or the neutral base if none is set).

default()

@spec default() :: t()

Return the neutral base theme.

font(ios_family, opts)

@spec font(String.t(), [{:from_file, Path.t()}]) :: font_spec()

Builds a font token value: the iOS PostScript name as given, paired with the Android resource name computed from the bundled file via Mob.Font.android_resource_name/1 — the same function mob_dev's asset planner uses when it copies the file into res/font/. One computation, used on both the build side and the theme side, so the two can't drift apart the way a hand-typed Android name could.

fonts: %{
  heading: Mob.Theme.font("Inter-Bold", from_file: "priv/fonts/Inter-Bold.ttf")
}

resolved_palette(theme \\ current())

@spec resolved_palette(t()) :: %{required(atom()) => non_neg_integer()}

Returns the active theme's palette resolved to ARGB integers — semantic tokens (:primary, :on_surface, …) walked through the theme's color map and then through Mob.Renderer.colors/0. Used to push concrete values to the native side (Mob.Theme.set/1 does this automatically; callers usually don't need to invoke this directly).

set(theme)

@spec set(t() | module() | {module(), keyword()} | keyword()) :: :ok

Set the active theme. Accepts:

  • A compiled %Mob.Theme{} struct
  • A theme module (any module exporting theme/0, e.g. MobThemes.Obsidian)
  • A {module, overrides} tuple
  • A keyword list of overrides against the neutral base

The module and {module, overrides} forms are remembered (see Mob.Theme.AdaptiveWatcher): when the module is the registered follow-OS theme, an OS light/dark flip resolves it again, keeping the overrides. A struct or keyword list is a fixed theme and is never re-resolved, even if it equals what the adaptive module returns.