LeCodesdocs

Theme — app-wide style variables

One app-wide variable table, resolved live by the UI core. theme(values) merges variables in and returns typed accessors whose values are the var() strings; any style value can reference a variable, and re-calling theme() re-styles the running UI in place — dark mode, a brand switch, or a per-user accent is one more call, not a rebuild. This page is the canonical home of theme(); the property vocabulary the variables plug into is in styling.md.

At a glance

The standard shape is a small tokens module every screen imports — palette in theme(), scale as plain constants:

TypeScript
// theme.ts — define once, export the accessors
const palette = {
  bg: "#F4F6F5", card: "#FFFFFF", border: "#E4E8E6",
  text: "#131A17", muted: "#606B65",
  accent: "#15A34A", accentSoft: "#E7F6ED", onAccent: "#FFFFFF",
}

// System keys: default text color + what kit components (UITabs bar) tint themselves with.
theme({ color: palette.text, primaryColor: palette.accent, mutedColor: palette.muted })

export const colors: { [K in keyof typeof palette]: string } = theme(palette)
export const font = {    // type scale — spread into styles: .style({ ...font.h2 })
  h2: { fontSize: 22, fontWeight: 700 }, body: { fontSize: 16 },
  small: { fontSize: 14 }, tiny: { fontSize: 12, fontWeight: 500 },
}
TypeScript
// any screen
import { colors, font } from './theme'

const card = UIColumn(
  UIText("Total").style({ ...font.small, color: colors.muted }),
  UIText("$1,240").style({ ...font.h2 }),
).style({ bgColor: colors.card, border: `1px solid ${colors.border}`, borderRadius: 16, p: 16 })
TypeScript
// later, anywhere — every style referencing a token repaints in place
theme({ bg: "#0C0E13", card: "#161A23", text: "#EEF1F8", border: "#2A3040" })   // dark mode

colors.card is the literal string "var(--card)" — the host resolves it at style-apply time. Spacing and type scales stay plain TS constants: a theme variable is for what should be able to change while the app runs.

Note

the accessor export is annotated { [K in keyof typeof palette]: string } on purpose. theme() infers literal types ("var(--accent)"), and a helper written as (color = colors.accent) would otherwise narrow its parameter to that one token.

Defining variables

TypeScript
theme(values): accessors      // MERGES into the live table; keys not mentioned keep their values
  • Strings pass through — colors, font names, whole expressions ("max(safe-top, 24px)").
  • Numbers are lengths (logical px).
  • null removes a key (comfort knobs reset to their built-in defaults).
  • Env names (safe-top…, vw/vh/vmin/vmax) are host-owned facts, never theme keys — writes are skipped with a console warning.
  • One non-style key: replaceTransition below.

Every call returns accessors for the keys it defined; the returned object is all you need to export from a tokens module.

Reading variables

TypeScript
label.style({ color: T.accent })                     // accessor — T.accent === "var(--accent)"
label.style({ color: "var(--accent)" })              // raw string form — same thing
icon.style({ tintColor: "var(--accent, #0A84FF)" })  // fallback applies while the key is unset
list.style({ pl: "calc(var(--comfort-left) * 2)" })  // composes into calc()/min()/max()

var() works in any UI style value and nowhere else — see Pitfalls for what that excludes. A var() for a key that was never defined resolves to its fallback, or to nothing.

System variables

Some keys are read by the SDK itself, not just by your styles. They're pre-typed as static properties on the global — theme.color, theme.primaryColor, … — so they need no tokens module.

Default text — two keys drive every UIText that doesn't set its own value, so a light theme is one line instead of color: "black" on every label:

TypeScript
theme({ color: "#1C1C1E", fontFamily: font("manrope") })   // all unstyled text follows

Unset, they equal the host defaults (white text, host font). An element's own color / fontFamily always wins. Screen background stays per-screen (bgColor) — style it explicitly.

Kit componentsUITabs styles its bar entirely through theme variables (with dark fallbacks, so an unthemed app still looks right):

Key Drives
primaryColor active tab icon + label (and the general "app accent" for kit components)
mutedColor inactive tab icon + label / secondary content
tabbarBg tab bar surface
tabbarBorder tab bar top hairline
screenBg default background behind tab screens
badgeColor badge dot / count pill

Comfort knobs"comfort-top" / "comfort-bottom" / "comfort-left" / "comfort-right" tune the safe-area comfort tokens (plain lengths only):

TypeScript
theme({ "comfort-left": 20, "comfort-right": 20 })   // this app's page gutter is 20
Note

the bare style token (pt: "comfort-top") applies the safe-area formula; var(--comfort-top) reads the raw knob value (default 12) for manual composition.

Live re-theming

Because every var() re-resolves in place, a theme() call restyles whatever is currently on screen — no screen rebuild, no prop threading, no call site that can be forgotten. That makes the table the right home for anything decided at runtime:

TypeScript
// Dark mode: a second call.
const setDark = (dark: boolean) => theme(dark ? darkPalette : lightPalette)

// A server-driven accent: derive a few variables from one hex and rewrite them.
// Every style that says var(--accent) / var(--accentSoft) follows — including screens
// that are already mounted.
const applyBrand = (hex: string | null) =>
  theme(hex ? { accent: hex, accentSoft: soften(hex) } : { accent: "#15A34A", accentSoft: "#E7F6ED" })

Writes land immediately, whenever they happen — there is no ordering hazard between a theme() call and screens being built before or after it.

replaceTransition

One key is consumed by the SDK router instead of entering the var table: the app-wide default transition for Router.replace (out of the box "none").

TypeScript
theme({ replaceTransition: "fade" })   // any transition name or custom spec; null resets

An explicit per-call Router.replace(screen, { transition }) still wins. It returns no accessor — var(--replaceTransition) is not a style value. See screen-router.md.

Pitfalls

TypeScript
// ✗ var() outside UI styles — SVG XML, engine colors, native-view params get the literal string
SvgSource(`<circle fill="${colors.accent}" ...>`)      // renders nothing useful
sprite.color = colors.accent                           // engine APIs parse hex/packed int only
// ✓ keep the raw palette object exported next to the accessors for those consumers
SvgSource(`<circle fill="${palette.accent}" ...>`)

// ✗ theming an env name — host-owned, skipped with a warning
theme({ "safe-top": 0 })

// ✗ a formula in a comfort knob — knobs take plain lengths
theme({ "comfort-bottom": "max(safe-bottom, 16px)" })
// ✓ write the formula inline in the style, or as your own variable
theme({ tabInset: "max(safe-bottom, 16px)" }); bar.style({ pb: "var(--tabInset)" })

See also

  • Styling — the property vocabulary and value forms variables plug into
  • Style classes — per-element named states (theme = app-wide, classes = per-element)
  • Fontstheme({ fontFamily: font(...) }), the default-font mechanism
  • UIPager & UITabs — the tab bar the kit variables style