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:
// 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 },
}// 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 })// later, anywhere — every style referencing a token repaints in place
theme({ bg: "#0C0E13", card: "#161A23", text: "#EEF1F8", border: "#2A3040" }) // dark modecolors.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.
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
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).
nullremoves 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:
replaceTransitionbelow.
Every call returns accessors for the keys it defined; the returned object is all you need to export from a tokens module.
Reading variables
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:
theme({ color: "#1C1C1E", fontFamily: font("manrope") }) // all unstyled text followsUnset, 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 components — UITabs 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):
theme({ "comfort-left": 20, "comfort-right": 20 }) // this app's page gutter is 20the 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:
// 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").
theme({ replaceTransition: "fade" }) // any transition name or custom spec; null resetsAn 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
// ✗ 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)
- Fonts —
theme({ fontFamily: font(...) }), the default-font mechanism - UIPager & UITabs — the tab bar the kit variables style