Follow this guide to learn how Layers data is structured in JSON.
This guide is the complete reference for authoring Layers data in JSON. It is progressive: it starts with plain values and works up to the full composition toolkit (linking, states, inheritance, overrides, namespacing).
One vocabulary note up front: Layers calls every node a style. The name is a souvenir of the project's theming origins; a style can just as well describe a character, a config, or a string table. Layers never assigns meaning to your attribute names. The examples below deliberately mix domains (game data, configuration, UI) because the engine does not care which one you are in.
An attribute is a named, typed value:
"Max Health": 100
Four value types are supported: numbers, booleans, strings, and lists of strings. The name is yours; Layers stores and resolves the value but never interprets it.
A style is a named node: a set of attributes plus child styles. Nesting styles is how you give your data hierarchy, and a tree of styles is your data.
{
"Character": {
"Max Health": 100,
"Health": 100,
"Armor": {
"Head": {},
"Body": {}
}
}
}
Styles are addressed by paths with / between names: Character/Armor/Head. The path separator is / and only /; other punctuation in a key is part of the name itself.
The top-level keys of a file always declare styles. Attributes exist inside styles, never at file level.
The parser decides what a key is by looking at its value:
| Value | Result |
|---|---|
| number, bool, string, or array | attribute |
object containing :-prefixed keys |
attribute (with states; see below) |
object whose -> holds a plain value |
attribute (that value is the base) |
string starting with S: |
child style (inheriting that base) |
object whose -> holds an S: reference |
child style (inheriting that base) |
any other object, including {} |
child style |
A key prefixed with ? is reserved for a future feature (modifiers) and should not be used.
->)Most values are written directly. But when a key's value must be a JSON object (to hold states, or to inherit while also declaring children), the object form needs a way to carry the "main" value. That is the -> key:
// Value applied directly:
"Thickness": 0
// Same value, object form (needed once states are involved):
"Thickness": {
"->": 0,
":Selected": 3
}
Inside a style object, -> instead carries the style's inheritance reference ("->": "S:Base"); see Style inheritance below.
L:)An attribute's value can be another attribute's value. Prefix the target's path with L::
"Text Color": "L:Theme/Foreground"
Text Color now reads whatever Theme/Foreground holds. Change the target (or re-point it) and every attribute linked to it follows on the next read. Links chain: if the target is itself linked, resolution follows through to the end.
Link paths use / between styles, with the final segment naming the attribute. Links are resolved once, after all styles are loaded (resolve_links(); see the integration guide), and resolution is namespace-scoped: the referrer's own namespace is tried first, then the global root (see Namespacing style sets below).
States give one attribute several values, selected at read time by which states are currently active. The reader supplies the active states; like attribute names, the state vocabulary is entirely yours.
Declare states inside the attribute's object form, prefixed with :. The -> key holds the base value used when no state matches:
"Base URL": {
"->": "http://localhost:8080",
":Staging": "https://staging.example.com",
":Prod": "https://api.example.com"
}
A config tree with states like these is an entire dev/staging/prod setup in one file: the application reads with {"Prod"} active and every stated attribute resolves accordingly.
A state key can require several states at once by joining names with ::
"Border.Thickness": {
"->": 0,
":Desktop": 8,
":Desktop:Maximized": 0
}
:Desktop:Maximized applies only when both Desktop and Maximized are active.
When a value is read with a set of active states:
{Desktop, Maximized} active, :Desktop:Maximized (two names) beats :Desktop (one).-> base value.A state's value can be anything a base value can be, including a link:
"Fill": {
"->": "L:Theme/Primary",
":Checked": "L:Theme/Foreground"
}
When a key contains a parenthesized, comma-separated group, it expands into one key per combination:
"Margins.(Left,Right)": 5
This declares Margins.Left and Margins.Right, both 5. Multiple groups expand combinatorially:
// The space between the groups is significant!
"Corner Radii.(Top,Bottom) (Left,Right)": 8
That declares all four corner attributes. Expansion works for child styles too:
"Armor.(Head,Body,Arms,Legs)": {}
declares four (empty) child styles, and
"(Icon,Text)": "S:Label"
declares two child styles that each inherit Label.
A dot in an attribute name is a naming convention for related attributes:
"Margins.Left": 5,
"Margins.Top": 5,
"Margins.Right": 5,
"Margins.Bottom": 5
The engine treats each of these as one atomic name; the dot has no structural meaning and cannot be used to address parts of a name. Dotted names read well, group naturally with key-expansion ("Margins.(Left,Top,Right,Bottom)": 5), and keep related values sorted together. That is all they do.
S:)Inheritance makes styles reusable: a style adopts a base's attributes and children, then overrides and extends on top.
There are two reference forms:
"S:Character", resolving within the style's own namespace first, then globally."S:Vortex/Box", reaching into another library's namespace.Note that which file a style lives in plays no part in resolution. Files are purely organizational.
Use the reference as the value itself, or as the -> key when the style also declares its own content:
{
"Entity": {
"Model": ""
},
"Character": {
"->": "S:Entity",
"Max Health": 100,
"Health": 100
},
"Humanoid": {
"->": "S:Character",
"Armor.(Head,Body,Arms,Legs)": {}
},
"Player": "S:Humanoid"
}
Player resolves the whole chain: it has Model from Entity, the vitals from Character, and the armor slots from Humanoid.
Merging is recursive and order-preserving: an attribute or child you redeclare overrides in place; base children you do not touch are kept, in the base's order; new children are appended. States merge too: a derived style inherits the base's states and can override any of them by redeclaring the same : key, or add new ones.
Because a derived style stores only what it changes, inheritance doubles as an instance mechanism. A saved-game hero is just a diff over Player:
{
"Save": {
"Hero": {
"->": "S:Player",
"Model": "player-hero",
"Max Health": 200,
"Armor.Head": "S:Leather Hood"
}
}
}
Save/Hero reads Max Health 200 (overridden) but Health 100 (inherited). Writing a value on the instance at runtime never touches the base.
Within one include(), the whole set is built before bases resolve, so styles may reference each other in any order, across any files. Across namespaces, resolution is eager: a set that references S:Vortex/... must be included after the Vortex set.
A key containing / targets a nested node without re-nesting the JSON. There are two placements with two behaviors.
Inside a style object, a path key is shorthand nesting. It is split and built as ordinary children at parse time, and then participates in inheritance merging like anything else:
{
"My Window": {
"->": "S:Window",
"Titlebar/Exit Button/Icon/Color": {
"->": "L:Theme/Foreground",
":Selected": "#eb2f26"
}
}
}
This reaches through the inherited Titlebar and overrides one attribute of one deeply nested node, leaving everything else intact. Note that the terminal key must carry its states in the object form shown; a : in the path itself (".../Color:Selected") would just create an attribute literally named Color:Selected.
At the top level of a file, a path key is an appendage: it is held back until after all inheritance in the set has been resolved, then grafted onto the node the path names. If the target child already exists (including one materialized by inheritance), the appendage overrides it in place; the target keeps its own base chain, so a patched node stays the kind of thing it was. If the target child does not exist, the appendage is added as a new node.
// app.json
{
"App": {
"Main Window": { "->": "S:Vortex/Main Window" }
}
}
// greeting.json: patches a node that inheritance created
{
"App/Main Window/Content/Greeting": {
"->": "S:Vortex/Text",
"Text": "Hello, Vortex"
}
}
In a namespaced set, top-level path keys are automatically qualified into that namespace. An appendage overrides a matching attribute completely: its base value, its link, and its state values, while states the appendage does not mention are kept.
A link's target is just another attribute, so a single, shared node can stand in front of many values, and everything linked to it follows when you change it. This is the indirection node (or mount) pattern, and it is how you build things like theming on top of Layers.
Define an ordinary top-level style whose attributes are the shared values:
// theme.json
{
"Theme": {
"Foreground": "#e3e3e3",
"Primary": "#36393f",
"Secondary": "#2f3136",
"Tertiary": "#25272b"
}
}
Then link to it from anywhere by its path:
"Fill": "L:Theme/Primary"
Theme is not reserved or special; it is just a style that happens to be named Theme, resolved like any other path. What makes it a mount is how it is used: many attributes across the tree link to its handful of attributes, so re-pointing (or re-valuing) those few re-skins everything downstream at once, with no re-resolution of the tree, since the links already point at the node.
This is the pattern Vortex builds its theming on (the Theme node above is its palette; swapping themes re-points those attributes). The same shape fits any shared, swappable set of values: a string table for i18n, a brand kit, a density scale, or per-environment endpoints. Wherever you want one place to change that many values follow, mount a node.
Styles from every include() load into one shared tree, so two libraries that each define a top-level Box would collide. To keep a library's names to itself, give its styles a namespace at load time:
lController.include("/styles/vortex", "Vortex"); // Box -> Vortex/Box, ...
lController.include("/styles/app"); // app styles, un-namespaced
A namespaced include parents its top-level styles under a node of that name, so they resolve at Vortex/Box, Vortex/Button, and so on. References then resolve namespace-first, then globally:
"->": "S:Box" from a Vortex style finds Vortex/Box."->": "S:Vortex/Main Window".Theme out of any namespace (load it un-namespaced); "L:Theme/..." then resolves from inside any namespace via the global fallback. This is why a library's widgets can link L:Theme/Foreground without knowing which app hosts them.Un-namespaced includes land at the root and resolve globally, so namespacing is opt-in per include().
~)A key prefixed with ~ is not parsed into attributes or children. Its value is stored verbatim as JSON metadata on the style and is readable from C++ (LStyle::extension(key), has_extension(key)). Layers gives extensions no semantics; they exist for your tooling: annotations, editor hints, documentation, anything you want to ride along with a node.
"Button": {
"~docs": "Primary action button. Inherit and override Fill for variants.",
"Fill": "L:Theme/Tertiary"
}
This feature is experimental: the authoring format may still change, and no shipping project uses it yet.
User styles are extra style sets that end users can toggle on top of developer styles. A user style is a folder containing a style.json, loaded from a directory of such folders via lController.load_user_styles(path). The file's single top-level key names the user style; its children name the developer style paths to patch:
// user-styles/Square Corners/style.json
{
"Square Corners": {
"Button": { "Corner Radii": 0 },
"Window": { "Corner Radii": 0 }
}
}
lController.toggle_custom_style("Square Corners") applies or removes the patch at runtime. Applied values sit above everything else in value resolution (before states, links, and base values), and toggling off restores the original values. Matching is by exact path.
Loading. include(path) loads every .json file under path, recursively. File boundaries are invisible to resolution: how you split styles across files and subdirectories is purely organizational. Files whose names start with _ are skipped.
Error behavior. Layers currently favors resilience over strictness:
L: link whose path does not resolve is reported to stderr and left unresolved; reads fall back to the attribute's own base value. The full list is queryable from C++ (lController.unresolved_links(); see the C++ guide).S: base that cannot be found is silently skipped; the style keeps only what it declares itself.find_style, find_attribute) returns null.as<T>()) throws; as_if<T>() returns null instead.Ship the styles a reference needs before resolving, or the fallbacks above will hand you defaults. Checking unresolved_links() after resolving (or watching stderr) catches that early.
Everything above composes. Here is a small but complete UI style set: a theme mount, shared widget bases, a window, and an app-specific override.
// theme.json
{
"Theme": {
"Foreground": "#e3e3e3",
"Primary": "#36393f",
"Secondary": "#2f3136",
"Tertiary": "#25272b"
}
}
// widgets.json
{
"Label": { "Color": "L:Theme/Foreground" },
"Button": {
"Corner Radii": 3,
"Fill": "L:Theme/Tertiary",
"(Icon,Text)": "S:Label"
}
}
// window.json
{
"Window": {
"Border.Thickness": { "->": 8, ":Maximized": 0 },
"Corner Radii": { "->": 5, ":Maximized": 0 },
"Fill": "L:Theme/Primary",
"Titlebar": {
"Fill": "L:Theme/Secondary",
"(Minimize,Maximize,Exit) Button": "S:Button"
}
}
}
// app.json
{
"My App Window": {
"->": "S:Window",
"Titlebar/Exit Button/Icon/Color": {
"->": "L:Theme/Foreground",
":Selected": "#eb2f26"
}
}
}
Reading My App Window with no states active yields a bordered, rounded window whose colors all come from Theme; activate Maximized and the border and rounding drop to zero; select the exit button and only its icon turns red. Swap the Theme node's values and the entire tree re-skins.
This is exactly how Vortex declares cross-platform interfaces, with the styles doubling as the widget tree itself. To see the pattern taken to a full framework, read the Vortex styling guide.