Style (asset type)
When to use one
- You're branding a UI surface (game theme, editor theme, dialog theme) and want a named token set widgets pick up automatically.
- You want a swappable theme — light vs dark, normal vs accessibility contrast.
- You want designers to edit tokens (
color.primary,spacing.md) without touching widget code.
If you need per-widget overrides, those go inline on the widget, not
in a style. If you need shader-driven looks, that's a .shader +
.material.
Where it lives
- Source:
/zero/source/.../<name>.style/ - Identity:
<name>(the.stylesuffix strips). - Folder shape:
init.luau(orinit.lua) — returns the module carrying the token table. Required.README.md— instance documentation. Required..metadata— agent-editable tags + free-form fields. Required.
How to create one
asset.create("style", "<name>")
-- Creates: /zero/source/<name>.style/
-- init.luau (token scaffold: color / spacing / font)
-- README.md (instance README template)
-- `folder` places it in a subfolder of /source instead of the root:
asset.create("style", "<name>", { folder = "themes" })
How it operates
- Registration. Writing
init.luauinto a.style/folder registers the asset — it is listable, inspectable, and referencable by identity. - Token shape.
init.luaureturns a module exposingM.tokens, one nested table per token category:color.<name>/spacing.<name>/font.<name>are the conventional three; a style may declare any category it wants.- A value is a literal (a
"#rrggbb"colour string, a number, an array) or a"$otherToken"reference resolved during the cascade. An optionalM.stylesmaps a selector —<widgetType>,<widgetType>.<className>, or a bare.<className>— to a block of style properties, each value a literal or a$tokenreference.
- Cascade. The stylesheet resolves a widget's look in the order
type→type.class→.class→ inline props. Widgets select classes with theirclasses(orclass) prop. - Application. The UI theme surface takes the style's table:
Theme.register(name, tokens_and_styles)from@builtin::modules.zui.themeresolves every$tokenin Luau and pushes the flat result throughui.registerTheme. From thereui.setTheme(name)makes it the active theme, andui.useStyles(name)applies a registered theme's classes additively without changing the active one.ui.defineStyle/ui.defineStylesdefine individual classes directly. - Hot reload. Editing
init.luaureloads the asset; re-register the style for the change to reach the live stylesheet.
Discovery
asset.list("style")— every registered style.asset.inspect("<name>")—tokenCategories(the top-level keys of the style'sM.tokensliteral, read from its source text), source, and this type README.cat /zero/source/<name>.style— same summary.styleRef:getInitScript()— the entry script's raw text.
Authoring conventions
- Write colours the way the theme system reads them —
"#rrggbb"/"#rrggbbaa"hex orrgba(r,g,b,a)strings. - Name tokens semantically (
color.primary,color.danger,spacing.gutter), not by visual value (color.blue,spacing.eight). Semantic names survive theme swaps. - Keep token names stable. Widgets reference tokens by name — renaming a token forces every consumer to update.
- Document
light/darkvariants under separate.style/folders with paired names (game.style,gameDark.style) rather than switching values within one style.
Common pitfalls
- Unknown
$tokenreference. A"$name"with no matching token fails the cascade —Theme.registerreturnsfalse, "token 'a': unknown token: $name"and the style never reaches the stylesheet. Read the error rather than the render. - Token reference cycles.
$a→$b→$ais reported astoken 'a': cycle detected: a → b → aat register time. - Missing tokens. A widget asking for
color.warnwhen the style doesn't define it falls back to the active theme. Inspect the style to confirm the token surface.
Related types
.component—UiPaneland other widget components consume styles..material/.shader— for 3D surface looks (different cascade system)..package— to ship styles + components together as a theme package.