Learn how Vortex interfaces are declared, styled, and themed with Layers.
Vortex interfaces are declared in Layers .lyr files. Styles are not decoration applied to widgets built elsewhere. Styles are the structure: a style names the widgets, nests them, arranges them, and colors them. C++ code supplies behavior and data; .lyr files supply everything you see. (In Layers, a style is just a layer; Vortex is what makes layers mean UI.)
This guide covers the authoring model. For the Layers language itself (inheritance, links, states, expansions, groups), see the Layers language reference.
The framework ships a library of widget styles in styles/widgets/: one file per widget, v<widget>.lyr for primitives (vbox.lyr, vtext.lyr, vbutton.lyr, …) plus composite building blocks (window.lyr, titlebar.lyr, vsettingsmenu.lyr, …). Shared values that aren't widgets, like the Theme node, live in styles/mounts/.
The framework's own styles load under the Vortex namespace (Vortex/Box, Vortex/Button, …), so within the library they reference each other by bare name. A node's widget type is decided by the primitive its style ultimately inherits. This is the framework's Button:
Button << Box:
Layout.Direction: "Row"
Layout.Spacing: 4
Fill: Theme/Tertiary
Corner Radii.(Top,Bottom) (Left,Right): 6
Padding.(Bottom,Left,Right,Top): 10
Text Color: Theme/Foreground
Three relationships do the work:
Name << Base: is inheritance: the layer adopts everything from the base (child layers included) and overrides on top. Bare within a namespace (<< Box); an app reaches into the library with a qualified base (<< Vortex/Box).Titlebar << Titlebar inside Main Window embeds a titlebar as a child widget. With no block after it, the child is an unchanged copy of its base; add an indented block to override.Fill: Theme/Tertiary is a theme link: it resolves to the framework's shared Theme node (a global mount kept out of any namespace, so it resolves from anywhere), which the active theme's palette drives (see Themes).An app's styles load additively on top of the framework library and un-namespaced, so they inherit and compose any library style through a Vortex/-qualified base. The root convention is an App layer that inherits Vortex/App and lists the app's top-level widgets:
App << Vortex/App:
Icon: "logo_myapp"
Name: "My App"
Main Window << Vortex/Main Window
Theme Directories Dialog << Vortex/Theme Directories Dialog
Icon (an icon file name) and Name are what the titlebar's app tab shows, so overriding them brands the shell.
build_app() activates it with app.set_root_style("App"); the platform shells pass the app's style directories alongside the framework's (see the app development guide).
Inherited structure would be useless if you couldn't reach inside it. A declaration whose name is a path reaches into inherited children: it overrides their attributes and states, or adds children that don't exist yet. This is the demo's window:
Demo Window << Vortex/Main Window:
App Pane/Content:
Label << Vortex/Text:
Text: "Hello World"
App Pane comes from Vortex/Main Window. Content doesn't exist there, so it's created, and Label is added inside it. (A layer without a primitive base, like Content here, renders as a Box.)
This is how an app customizes the framework's shell without copying it: inherit Main Window (under its own window name, if it likes), then override and add by path. Child order is preserved through the merge: inherited children keep their base's order, and new children follow them.
Any attribute can carry per-state values. The resolver folds in the active states (platform, window, and interaction), and the most specific match wins:
Border << Detail:
Thickness: 0
@Desktop: 8
@Desktop&Maximized: 0
States the framework drives for you:
@Desktop, @Web, @Mobile (set by each platform shell).@Maximized; @Narrow below the responsive width breakpoint; @ControlsHidden while chrome is hidden away on scroll.@Selected (titlebar tabs), @Checked (check boxes), @Expanded (collapsible drawers), @Open (sliding sheets), and per-row @First/@Last inside generated lists.App code can toggle its own named states with app.toggle_state(path, state), passing the state's name without the @ (e.g. "Selected").
On mobile, resolved pixel values are additionally auto-scaled (icons 1.5×, fonts 1.25×, other metrics 1.25× by default), unless an attribute has an explicit @Mobile state, which wins verbatim. See the layout guide.
Some layers describe part of a widget rather than a widget of their own. They inherit the Vortex/Detail marker, which keeps them out of the render tree: their parent reads their attributes, and they never become widgets (or list row templates). Border is one, which is why borders are written as Border/Thickness and Border/Fill rather than as attributes of the widget itself.
A Fill is a solid color. For a gradient, use a Fill. group of two stops instead, each a position (0 to 1) and a color:
Border << Detail:
Fill.
Start: 0
Start Color: "#3a3c42"
End: 1
End Color: "#42454d"
When a layer has both, the gradient wins, so a style can add stops over a solid fill it inherited without blanking it. The shell's window and dialog borders link their stops to the theme's Gradient group (Start: Theme/Gradient.Start, and so on).
Theming is a Vortex feature built on top of Layers; Layers itself has no notion of a theme. A theme is a small JSON file holding the palette plus metadata. Themes stay JSON because Vortex reads them itself, rather than loading them through Layers:
{
"Dark": {
"_meta": {
"publisher": "huntrsoftwarellc",
"type": "dark"
},
"Foreground": "#e3e3e3",
"Gradient.Start": 0,
"Gradient.Start Color": "#3a3c42",
"Gradient.End": 1,
"Gradient.End Color": "#42454d",
"Primary": "#36393f",
"Secondary": "#2f3136",
"Tertiary": "#25272b"
}
}
Those attributes are the canonical palette: the vocabulary every style links to (Theme/<Attribute>):
| Attribute | Purpose | Typical elements |
|---|---|---|
| Foreground | Strong contrast against all backgrounds | Icons + text |
| Gradient | Accent surface for borders/emphasis (a two-stop group: Start, Start Color, End, End Color) |
Window & dialog borders |
| Primary | Main backgrounds | Windows, dialogs, pages |
| Secondary | Sub-element backgrounds | Headers, sidebars, sections |
| Tertiary | Interactive backgrounds | Buttons, tabs, text editors, slider handles |
Themes are identified as "<Name> (<publisher>)", e.g. "Dark (huntrsoftwarellc)". The framework bundles Dark and Light plus accent themes (Blue, Green, Indigo, Orange, Red, Violet, Yellow).
Every Theme/... link in every style resolves once, at load, to a shared Theme node (styles/mounts/vtheme.lyr). Switching themes is therefore a core-only operation: app.set_active_theme(id) copies the chosen theme's palette onto that node, and every link already pointing at it picks up the new palette on the next render, with no per-link re-resolution and no per-platform code. The framework's settings menu lists loaded themes and switches on click, out of the box.
Themes are also runtime data: add_theme_json() / remove_theme() load and drop user themes, the shell's theme-directories dialog lets users point at their own theme folders, and each platform persists the active selection (QSettings, browser storage, SharedPreferences).
This section is the developer's account. The user-facing walkthrough (write a theme file, load it into any Vortex app, iterate on it) is the Custom Themes guide, which is also where the dialog's Learn More button lands.
Text Color: one asset, every theme.Text Color, the image recolors like an icon, otherwise its own colors are preserved. Raster images and video always render verbatim.Apps override or extend the shell's icon set by shipping their own icon directory, searched before the framework's (see the app development guide).