zgx

Theming

Theme tokens, light and dark, and the system’s accent, contrast and text size.

A style names a theme token as a string: bg = "$surface.raised", color = "$text.secondary", p = "$space.3", rounded = "$radius.md", font = "$font.mono", shadow = "$shadow.lg". The standard tokens, their file format, app tokens and costs are described in styling: Theming; every theme defines the standard tokens, so a script styled with them takes any theme.

ui.theme.tokens({ app = { card = "{surface.raised}", badge = "alpha({accent}, 0.2)" } })
ui.theme.default(ui.theme.load("brand.json"))   -- the app's look, unless the user picks another
return ui.column {
    bg = "$background", p = "$space.4", gap = "$space.2",
    ui.text { "Settings", text_size = "$font.size.xl" },
    ui.div { bg = "$app.card", rounded = "$radius.lg", h = 80 },
    ui.button { "Light", on_click = function() ui.theme.set("zgx-light") end },
    ui.column { theme = "zgx-light", bg = "$surface.raised", ui.text { "A light card" } },
}

Tokens are "$name" strings rather than values read from a theme table (theme.text.secondary) because a string is a reference: Rust interns it once, when the script is lowered, and the node styles itself from the token, so a theme switch restyles exactly the nodes that use the tokens that changed without calling the script, and without a function per property to make the value reactive. The strings work wherever a value does: in hover tables, in states, and returned from computed properties (bg = function() return if on:get() then "$accent" else "$surface.base" end). The type definitions list the standard tokens, so editors complete "$…" and flag misspelled ones.

  • ui.theme.set(name) applies a theme (and stops following the system); ui.theme.default(name) applies it unless the app has chosen one, so a reload does not undo a choice.
  • ui.theme.current is state holding the applied theme’s name: ui.theme.current:get() (tracked, so text or a computed property reading it follows) and ui.theme.current:set(name). ui.theme.kind() is "dark", "light", "high-contrast" or "high-contrast-light"; ui.theme.names() lists the themes.
  • ui.prefers is the system’s display settings, as CSS’s media queries read them. Each is a tracked state that follows the system: ui.prefers.reduced_motion:get() (prefers-reduced-motion; macOS’s Reduce motion, Windows’ animation effects off), ui.prefers.more_contrast:get() (prefers-contrast: more; macOS’s Increase contrast, Windows’ contrast themes) and ui.prefers.reduced_transparency:get() (prefers-reduced-transparency; macOS’s Reduce transparency). The themes’ tokens already take them: motion durations of zero, borders as strong as secondary text, and opaque, unblurred glass. A view with a backdrop blur paints its background solid then. Read them for what tokens do not cover, such as an anime timeline to skip.
  • ui.theme.load(path) reads a theme file beside the script (a zgx theme, or a VS Code colour theme) and returns its name. The host watches it: saving it restyles the window in place, keeping state and without reloading the scripts; a file that stops reading shows its error until it reads again.
  • ui.theme.register(table) registers a theme written in Luau ({ name, type, extends, tokens }, as a theme file); a module returning one reloads like any module.
  • ui.theme.tokens(table) declares the app’s tokens, each with a default written in standard tokens (terminal = { pane = "{surface.sunken}" }, nested or dotted), so that any theme styles the app; a theme may set them by name.
  • ui.theme.get(name) returns a token’s value (tracked): colours as "#rrggbbaa", numbers, font names, shadows as { x, y, blur, spread, color } lists. Canvas drawing and layout arithmetic use it.
  • ui.theme.follow_system({ dark = "…", light = "…" }) follows the system’s appearance; false stops.
  • theme = … on any element scopes its subtree: a theme’s name (a light card in a dark app), or a table of tokens to override, with theme = name (or extends) to start from (theme = { accent = "#e5484d" }). It may be a state or a function; the scope follows it. Tokens derived from an overridden one follow, and inherited text takes the scope’s colour.

The design lint. ui.theme.strict("warn") (or "error", or ZGX_DESIGN_LINT=warn) reports every style that hard-codes a colour, spacing, corner radius, type size, weight, font or shadow, with the token to use: `bg = "#232e40"` hard-codes a colour … "$surface.raised" is exactly this in the current theme. ui.raw(value, "reason") lets a deliberate exception through. Sizes, 0, "auto", percentages, state values and theme definitions are not checked. See Design systems.

A token nothing defines resolves to nothing (transparent, 0), and a debug build warns once. The terminal (zgx-desktop/examples/terminal: theme.luau, glass.json) and notes (examples/notes: glass.json) examples are written this way: their own themes reproduce their palettes, and ui.theme.set("zgx-light") restyles them whole.

Edit on GitHub