Layers 2.0 is archived. The latest version is 3.0. Read the 3.0 docs

Follow this guide to learn how Layers data is structured in JSON.

JSON Guide - Layers

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.

Attributes

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.

Styles

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.

How a key becomes an attribute or a child style

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.

The arrow key (->)

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.

Linking attributes (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).

Attribute states

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.

Multi-state combos

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.

Selection rules

When a value is read with a set of active states:

  1. A state key qualifies if every name it lists is active. Active states it does not list are ignored.
  2. Among qualifying keys, the one listing the most names wins. In the example above, with {Desktop, Maximized} active, :Desktop:Maximized (two names) beats :Desktop (one).
  3. If two qualifying keys list the same number of names, the tie breaks lexicographically (state keys are kept sorted). This is deterministic, but do not design data that relies on it; prefer a more specific combo that names the difference.
  4. If no key qualifies, the attribute falls back to its link, then to its -> 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"
}

Key-expansion

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.

Grouping attributes with dots

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.

Style inheritance (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:

  • A bare name, "S:Character", resolving within the style's own namespace first, then globally.
  • A namespace-qualified name, "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.

Instances as diffs

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.

Load order

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.

Overriding nested nodes with path keys

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.

Indirection nodes (mounts)

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.

Namespacing style sets

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:

  • Within a library, use a bare name: "->": "S:Box" from a Vortex style finds Vortex/Box.
  • Across libraries, qualify with the namespace: an app's "->": "S:Vortex/Main Window".
  • Shared mounts stay global. Keep an indirection node like 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().

Extensions (~)

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"
}

User styles (experimental)

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.

Loader rules and error behavior

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:

  • An 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).
  • An S: base that cannot be found is silently skipped; the style keeps only what it declares itself.
  • Looking up a missing style or attribute (find_style, find_attribute) returns null.
  • A file that fails to parse is reported to stderr and skipped; the rest of the set still loads.
  • Reading a value as the wrong type (as<T>()) throws; as_if<T>() returns null instead.
  • Reading with a state that no key matches returns the link or base value (rule 4 above).

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.

Worked example: styling a window

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.

Appearance
Theme
—