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

Follow this guide to learn how Layers can be integrated with different UI frameworks across different platforms.

Integration Guide - Layers

This guide covers how to load Layers themes and styles into an application or library. It draws on real integrations: vortex-desktop (Qt desktop), vortex-android (Kotlin via JNI), and vortex-web (JavaScript via WebAssembly).

There is no single canonical pattern. Pick the one that matches your platform and how much control you want over loading.

The basic API

Layers has a single global controller, accessed via lController (a macro for LController::instance()). Two calls cover the typical lifecycle, loading your styles and then resolving links once:

#include <Layers/lcontroller.h>

lController.include("/path/to/styles");        // load a directory of JSON styles
lController.root_style()->resolve_links();     // resolve every L: link once

After this, your UI code reads resolved values via LAttribute::as<T>(). Reads follow the links resolved above. To make a value change without re-resolving the tree (a theme switch, say), re-point a shared node's attributes and everything linked to it follows (the indirection-node pattern in the JSON guide).

When you load styles from more than one library, pass a namespace so their top-level names can't collide; the library's styles then live under that node and references resolve within it first, falling back to the global root:

lController.include("/styles/vortex", "Vortex");  // Box -> Vortex/Box, ...
lController.include("/styles/app");               // app styles, un-namespaced

See Namespacing style sets in the JSON guide for the reference rules (bare within a library, qualified across, mounts kept global).

If you compile your styles into your binary with LayersResourceCompiler (recommended for C++ libraries; see Pattern A), you'll use lController.include_internal() instead of include().

Pattern A: Static initializer + compiled styles

Use when: Your styles ship inside a C++ library and should work once that library is linked. No app-side init call required.

Example: VortexWidgets (the widgets library inside vortex-desktop).

How it works

  1. Author JSON style files under a styles/ directory in your library.
  2. List them in a .lrc (Layers Resource Config) file.
  3. During the build, invoke LayersResourceCompiler to generate styles.cpp.
  4. Add a static Initializer instance whose constructor calls the generated initResources_styles_*() function and lController.include_internal().
  5. Link styles.cpp and the initializer into your library.

The static instance constructs before main() runs, so styles are available the moment the library is loaded.

Files

widgets/styles/styles_vortex.lrc:

{
  "/styles/vortex": [
    "vbutton.json",
    "vdialog.json",
    "..."
  ]
}

widgets/CMakeLists.txt (snippet):

set(STYLES_LRC "${CMAKE_CURRENT_SOURCE_DIR}/styles/styles_vortex.lrc")
set(STYLES_CPP "${CMAKE_CURRENT_BINARY_DIR}/styles.cpp")

add_custom_command(
  OUTPUT ${STYLES_CPP}
  COMMAND $<TARGET_FILE:Layers::LayersResourceCompiler>
    ${STYLES_LRC} ${STYLES_CPP}
  DEPENDS ${STYLES_LRC}
  VERBATIM
)

add_library(VortexWidgets SHARED ${WIDGETS_SOURCES} ${STYLES_CPP})

widgets/src/vortex_widgets_init.cpp:

#include <VortexWidgets/vortex_widgets_init.h>
#include <Layers/lcontroller.h>
#include <Layers/lresources.h>

namespace Layers::Resources {
    extern void initResources_styles_vortex();
}

using namespace Vortex;

Initializer_WIDGETS::Initializer_WIDGETS()
{
    Layers::Resources::initResources_styles_vortex();
    lController.include_internal("/styles/vortex");
} static Initializer_WIDGETS vortex_widgets_initializer;

Tradeoffs

  • ✅ No init code in the application
  • ✅ Load order follows link order, so inheritance across libraries Just Works
  • ❌ Styles cannot be changed without a rebuild
  • ❌ Less control over when styles get loaded: fine for libraries, sometimes inconvenient for applications

Pattern B: Application-controlled runtime load + compiled styles

Use when: Your application wants explicit control over when styles are loaded, typically because it needs to combine bundled assets with files in user directories.

Example: the application layer in vortex-desktop, sitting on top of a compiled widgets library.

How it works

Widget styles are still compiled into a library via LayersResourceCompiler and loaded by that library's own static initializer (same as Pattern A). What differs is that additional style sets (and any user styles) are loaded explicitly at app startup, and links are resolved once everything is in:

void App::init() {
    // Bundled + user-directory styles (paths from resources / settings)
    lController.include(bundled_styles_path);
    for (const auto& dir : user_style_dirs)
        lController.include(dir);

    // Optional user styles the app lets users toggle on top
    lController.load_user_styles(user_styles_path);

    // Resolve every L: link once, after all styles are in
    lController.root_style()->resolve_links();
}

Paths come from Qt resources for bundled assets and from QSettings for user-added directories. Anything the app layers on top of Layers (theming, for instance) is data the app owns and swaps behind a mount node (see the JSON guide); it is not part of this loading step.

Tradeoffs

  • ✅ Application picks up content from any source (bundled, settings directories, asset paths)
  • ✅ Plays well with persistence: restore user-toggled styles, remember app-owned selections between launches, etc.
  • ❌ More boilerplate at startup
  • ❌ The application must include every style set before it calls resolve_links()

Pattern C: Pure runtime load (no resource compiler)

Use when: Your application is not built around a C++ main: Android with JNI, the web via WebAssembly, or any host that fetches assets at runtime.

Examples: vortex-android, vortex-web.

How it works

You expose a small bridge from the host language to Layers. The bridge underneath is just calls into lController:

bool load_styles_from_path(const std::string& path) {
    try { lController.include(path); return true; }
    catch (...) { return false; }
}

void resolve_styles() {
    lController.root_style()->resolve_links();  // once, after all styles are loaded
}

The host language is responsible for getting JSON onto a path Layers can read:

  • Android (JNI): Bundled style JSON ships in APK assets. At first launch the app copies them out to internal storage, then Kotlin calls LayersBridge.loadStylesFromPath(path) and LayersBridge.resolveStyles().
  • Web (Embind): Style JSON is fetched over HTTP, written to Emscripten's virtual filesystem (MEMFS), then Module.loadStylesFromPath('/styles') and Module.resolveStyles() are called.

Anything built on top of the styles (theming, string tables) is data the host feeds and swaps behind a mount node itself (see the indirection-node pattern in the JSON guide); Layers just resolves the styles.

After loading, the host queries Layers per UI element for resolved attribute values. vortex-android and vortex-web both expose helpers along the lines of:

resolveBoxStyle(stylePath, states) -> { fill, cornerRadii, borderThickness, ... }
resolveTextColor(stylePath, states) -> color string
getAttributeString(stylePath, attrName) -> string

The host then renders with native widgets (Compose on Android, DOM on web) using those values.

Tradeoffs

  • ✅ Works on any platform with a C/C++ FFI
  • ✅ Themes and styles can be added or swapped without rebuilding
  • ❌ You must design a bridge surface that fits the host's renderer, a one-time investment per platform
  • ❌ FFI boundary is loosely typed; resolution errors surface late

Choosing a pattern

Pattern Build-time embedding Host language Best for
A: Static init Yes (LRC) C++ Libraries shipping a fixed style set
B: Runtime load Yes for widget styles (LRC) C++ Apps merging bundled + user-supplied content
C: Pure runtime No Any (via bridge) Mobile, web, or any non-C++ frontend

Patterns can be combined. vortex-desktop uses LRC-compiled widget styles (Pattern A-style for its widgets library) but drives theme + user-style loading from the application (Pattern B).

A note on user styles

Layers also supports user styles: extra style sets that users can toggle on top of developer styles to override the UI. The relevant API is lController.load_user_styles() and lController.toggle_custom_style().

This feature is experimental. No shipping project uses it yet, and the authoring format may still change. See the User styles section of the JSON guide for the format; the two controller methods above are the entry points.

Appearance
Theme
—