ZOA UI Theming
Give your editor tools and game UI a shared visual style.
ZOA Foundry is the default theme: dark ink surfaces, violet controls, and copper accents. ZOA Swarms remains available as a separate preset. Choose a theme in Presentation → UI Theming, or edit a theme asset to suit your game.
A ZOAUiTheme asset holds the colors, fonts, spacing, and visual assets used by themed controls. Foundry generates a USS stylesheet from those settings and refreshes attached UI Toolkit panels when the theme changes.
Tooling
Editor tools
Regenerate Active Theme Stylesheet
Tools > ZOA > Advanced > Generate > UI > Themes > Regenerate Active Theme Stylesheet
Forces ZOAThemeStyleSheetGenerator to rewrite the active theme's variables sheet. The postprocessor normally does this on save; reach for the menu after an out-of-band edit.
Ensure Default Theme / Ensure Theme Settings
Tools > ZOA > Advanced > Generate > UI > Themes
Creates the Foundry default, the selectable Swarms preset, and the theme settings at their expected paths. This also runs automatically on editor startup.
Reset Default Theme to Canonical Values
Tools > ZOA > Advanced > Generate > UI > Themes > Reset Default Theme to Canonical Values
Restores the Foundry default asset’s palette, metrics, and visual settings from ZOAFoundryThemePreset.
ZOAThemeAssetPostprocessor
Runs on every import. When a ZOAUiTheme or ZOAUiThemeSettings asset is imported, moved, or deleted, it regenerates the affected stylesheet and refreshes attached roots. No menu entry, no manual step.
Declared ZOA dependencies (0)
How it works
Concepts
A shared theme package without ZOA dependencies
UI Theming has no ZOA package dependencies. This keeps shared theme contracts available to gameplay and editor packages without introducing a reverse dependency on either.
The package does not depend on Messaging. A consuming bootstrap registers the theme service against IZOAThemeService and IHudThemeProvider. In the editor, ZOAUiThemeEditorServiceBootstrap installs the resolver used by Workbench windows and wizards.
HUD components can use IHudTheme for the shared palette. They do not need a reference to the package that created the theme asset.
Choose the contract your UI needs
IHudThemeProvider is the narrow surface: ActiveTheme plus a ThemeChanged event, where the theme is six colours with no UI Toolkit types anywhere in the signature. A HUD bridge can compile against it without pulling UnityEngine.UIElements into its assembly, and it may legitimately see a null ActiveTheme before a theme loads.
IZOAThemeService supports UI Toolkit panels. Attach adds the generated variables stylesheet to a VisualElement root, while ResolvedTheme supplies fallback-resolved assets and metrics. HUD consumers can use the narrower theme contracts when they do not need panel operations.
Attach is idempotent per root, tracks roots through weak references so it never keeps a dead visual tree alive, and re-attaches automatically when the active theme changes. Callers do not need their own bookkeeping for hot reload.
Tokens are generated from the asset
When a ZOAUiTheme asset is imported, ZOAThemeAssetPostprocessor asks the stylesheet generator to update its --zoa-* variables. Attached panels then resolve their existing USS rules against the new values.
The postprocessor ignores imports that contain only the generated stylesheet, preventing a recursive generation loop. EnsureGenerated compares the new content with the existing file and writes only when it has changed.
ZOAUiTheme implements IHudTheme directly rather than through an adapter. WindowBackgroundColor maps to SurfaceBase, PanelBackgroundColor to SurfacePanel, SlotBackgroundColor to SurfaceEmbedded, BorderColor to BorderDefault, TextColor to TextPrimary, and AccentColor to Yellow, the canonical primary accent. A ZOAUiTheme reference can be handed to a HUD bridge as-is.
A panel needs two separate things
A runtime UIDocument paints nothing without a PanelSettings asset, and a PanelSettings without a base ThemeStyleSheet renders Labels with no glyphs at all. That is the panel-level concern, and ZOAUiPanelSettings.Ensure owns it: it loads the canonical PanelSettings from the Resources path UI/ZOA UI Panel Settings and guarantees it carries the runtime theme sheet at UI/ZOADefaultRuntimeTheme.
Attaching the design tokens is the element-level concern, and that is root.ApplyZOATheme(). Both are required and neither substitutes for the other. Skipping the first gives an invisible panel; skipping the second gives a visible panel painted in fallback colours.
The canonical panel assets live in another package's Resources folder and are loaded here by global Resources path, because Resources folders merge across packages at runtime. That is how the theming package supplies them without taking an asmdef dependency on their owner.
Players can pick a theme too
ZOAUiThemeSettings is the project-level answer to which theme is active, loaded from Resources/ZOA/ThemeSettings so any package can resolve it with a single Resources.Load. ZOAUiThemeRuntimeSelection layers a player-facing choice on top, persisting a theme id under the PlayerPrefs key ZOA.UI.Theme.ActiveThemeId and scanning the ZOA/ThemeS catalog folders for what is available.
ZOAUiThemeSelectorWidget builds the picker as a plain VisualElement, independent of the frontend options descriptor model. An options screen wants a rich preset picker with live apply; the scalar-valued options service is the wrong shape for that, so the widget owns the interaction and writes back through the theme service itself.
Setup
Workflow
- 01
Let the editor bootstrap seed the project
On editor startup, ZOAUiThemeBootstrap creates the Foundry default theme and project settings when needed. A one-time migration updates the old Swarms default while preserving custom themes. After migration, an explicit Swarms selection is kept. The theme menus can recreate missing assets.
- 02
Author or import a theme
Create a ZOAUiTheme through ZOA > UI > UI Theme, or import the UI Themes sample, which ships more than thirty pre-configured themes spanning Modern Warfare, Sci Fi, Cyberpunk, Dieselpunk, Fantasy, Wild West, Post-Apocalypse, Ancient World, and Cosmic Horror aesthetics, one folder per theme, each with a ZOAUiTheme asset. Point the settings asset's ActiveTheme at the one you want.
- 03
Save, and let the generator run
Saving the theme triggers ZOAThemeAssetPostprocessor, which regenerates the theme's variables stylesheet and prompts the service to re-attach every tracked root. There is no manual regenerate step in the normal loop.
- 04
Register a provider from your own bootstrap
The theming package cannot reach FoundryServiceRegistry. Construct a ZOAThemeService in a consuming package's bootstrap and register it against both IZOAThemeService and IHudThemeProvider, then install the resolver with VisualElementExtensions.SetThemeServiceResolver so ApplyZOATheme routes to the same instance.
- 05
Make each panel render-ready, then theme its root
In a UIDocument host's Awake or OnEnable, call document.EnsureZOAPanelSettings() for the panel-level requirement, then root.ApplyZOATheme() for the element-level one. Call both, in that order, on every panel.
- 06
Consume tokens from USS, not from literals
Write rules against var(--zoa-surface-panel), var(--zoa-border-default) and friends. The token-coverage guardrail fails the build on raw colour literals in rule bodies, so a theme swap changes the whole surface.
Reference
Key types
IHudTheme
interface
Six colours and an id. The single cross-package contract a HUD component consumes so it never imports the package that authored the theme.
- string ThemeId { get; }
- Color WindowBackgroundColor { get; }
- Color PanelBackgroundColor { get; }
- Color SlotBackgroundColor { get; }
- Color BorderColor { get; }
- Color TextColor { get; }
- Color AccentColor { get; }
IHudThemeProvider
interface
Publishes the active IHudTheme and raises an event when it changes. Resolved from the service registry by consumers; ActiveTheme may be null before a theme loads.
- IHudTheme ActiveTheme { get; }
- event Action<IHudTheme> ThemeChanged
IZOAThemeService
interface
The UI Toolkit side of theming. Attach the generated variables sheet to a panel root, read the fallback-resolved theme view, and react to theme changes.
- ZOAUiTheme ActiveTheme { get; }
- ZOAUiThemeResolvedView ResolvedTheme { get; }
- event Action<ZOAUiTheme> ThemeChanged
- void Attach(VisualElement root)
- void Detach(VisualElement root)
- void RefreshAttachedRoots()
- void ReloadFromSettings()
ZOAThemeService
service
The default implementation of both interfaces. Resolves the active theme from settings, loads the generated stylesheet from Resources, and re-attaches every tracked root on change. It never self-registers.
- ZOAThemeService()
- ZOAThemeService(ZOAUiTheme fallbackTheme)
- void SetActiveTheme(ZOAUiTheme theme)
- void ReloadFromSettings()
VisualElementExtensions
class
The extension methods most call sites use. ApplyZOATheme resolves the service through an installed resolver, falling back to an in-process instance so an unbootstrapped editor panel still renders themed.
- static void ApplyZOATheme(this VisualElement root)
- static void RemoveZOATheme(this VisualElement root)
- static void SetThemeServiceResolver(Func<IZOAThemeService> resolver)
ZOAUiPanelSettings
class
Makes a runtime UIDocument render-ready: canonical PanelSettings plus the base ThemeStyleSheet that UI Toolkit needs before a Label draws any glyphs.
- const string PanelSettingsResourcePath = "UI/ZOA UI Panel Settings"
- const string RuntimeThemeResourcePath = "UI/ZOADefaultRuntimeTheme"
- static PanelSettings LoadCanonical()
- static PanelSettings Ensure(UIDocument document)
- static void EnsureRuntimeTheme(PanelSettings settings)
- static PanelSettings EnsureZOAPanelSettings(this UIDocument document)
ZOAUiThemeResolvedView
class
Active theme projected over a fallback theme. Reach for this instead of reading ActiveTheme directly when you need sprites, textures, cursors, HUD metrics, or scalars, so every UI stack shares one set of fallback rules.
- ZOAUiTheme ActiveTheme / FallbackTheme / EffectiveTheme { get; }
- static ZOAUiThemeResolvedView FromActiveTheme(ZOAUiTheme activeTheme)
- static ZOAUiTheme LoadDefaultTheme()
- Sprite CrosshairSprite { get; } float CrosshairSize { get; }
- float HudBarHeight { get; } float HudBarSpacing { get; }
- Sprite ResolveArchetypeIcon(string archetypeId)
- bool TryResolveSlotLayout(string slotId, out Rect position)
ZOAUiThemeRuntimeSelection
class
Player-facing theme selection. Enumerates the theme catalog out of Resources and persists the chosen theme id in PlayerPrefs, separate from the project default in the settings asset.
- const string PlayerPrefsThemeIdKey = "ZOA.UI.Theme.ActiveThemeId"
- static List<ZOAUiTheme> LoadAvailableThemes()
- static ZOAUiTheme ResolveThemeForService(ZOAUiTheme projectDefault)
- static void PersistThemeSelection(ZOAUiTheme theme)
- static bool TryFindThemeById(string themeId, out ZOAUiTheme theme)
- static void ClearPersistedThemeSelection()
ZOAUiThemeSelectorWidget
class
Builds a runtime-safe theme picker as a VisualElement. Options control whether a pick is persisted, written back to the settings asset, and applied immediately.
- static VisualElement Build(Options options = null)
- class Options { Themes, ActiveTheme, ThemeService, PersistSelection, WriteThemeSettings, ApplyOnSelection, ThemeSelected }
Reference
Authoring assets
ZOAUiTheme
asset
The theme asset. One ScriptableObject carrying every --zoa-* token the project renders through: palette and tints, typography, spacing, radii, borders, surfaces, text roles, controls, transitions, shadows, focus, sprites, cursors, accessibility values, and HUD bar and notification values. It implements IHudTheme directly. Created via ZOA > UI > UI Theme.
- string ThemeId / DisplayName / Description, int Version, string CacheKey
- Color Yellow, Black, Dark, Slate, Red, Cyan, White, Dim (the nine canonical palette tokens)
- Color SurfaceBase / SurfacePanel / SurfaceEmbedded / SurfaceElevated / SurfaceHover
- Color TextPrimary / TextSecondary / TextAccent, BorderSubtle / BorderDefault / BorderStrong
- float UiScale, bool UseColorBlindMode, Color ColorBlindAccent
- Sprite CrosshairSprite, float CrosshairSize, Texture2D CursorDefault / CursorInteract
- Sprite GetArchetypeIcon(string archetypeId)
- bool TryGetSlotLayout(string slotId, out Rect position)
ZOAUiThemeSettings
asset
Project-level active-theme selector, resolved through a static Instance that loads from Resources/ZOA/ThemeSettings. In the editor it falls back to a project-wide asset search and then to the editor bootstrap, so a fresh clone is never themeless. Created via ZOA > UI > Theme Settings.
- static ZOAUiThemeSettings Instance { get; }
- ZOAUiTheme ActiveTheme { get; set; }
- bool RequireThemeInResources { get; }
- float WorkbenchThemeBackgroundOpacity / ChromeOpacity / ContentOpacity
- ZOAUiThemePaths.DefaultSettingsAssetPath = "Assets/Resources/ZOA/ThemeSettings.asset"
Usage
Examples
using System;
using UnityEngine;
using UnityEngine.UIElements;
using ZOA.Messaging;
using ZOA.UI.Theming;
public sealed class VitalsPanel : MonoBehaviour
{
private IHudThemeProvider _themes;
private VisualElement _panel;
private void OnEnable()
{
if (!FoundryServiceRegistry.TryResolve<IHudThemeProvider>(out _themes))
return;
_themes.ThemeChanged += Repaint;
// ActiveTheme is legitimately null before a theme loads.
Repaint(_themes.ActiveTheme);
}
private void OnDisable()
{
if (_themes != null) _themes.ThemeChanged -= Repaint;
}
private void Repaint(IHudTheme theme)
{
if (theme == null || _panel == null) return;
_panel.style.backgroundColor = theme.PanelBackgroundColor;
_panel.style.borderBottomColor = theme.BorderColor;
_panel.style.color = theme.TextColor;
}
}using UnityEngine;
using UnityEngine.UIElements;
using ZOA.UI.Theming;
[RequireComponent(typeof(UIDocument))]
public sealed class ObjectiveHud : MonoBehaviour
{
private void Awake()
{
var document = GetComponent<UIDocument>();
// 1. Panel-level: PanelSettings + base ThemeStyleSheet, or the
// panel paints nothing and Labels render no glyphs.
document.EnsureZOAPanelSettings();
}
private void OnEnable()
{
var root = GetComponent<UIDocument>().rootVisualElement;
// 2. Element-level: attach the generated --zoa-* variables sheet
// so this tree's USS resolves against the active theme.
root.ApplyZOATheme();
}
}using ZOA.Messaging;
using ZOA.UI.Theming;
public static class ProjectThemeBootstrap
{
public static void Install()
{
var service = new ZOAThemeService();
service.ReloadFromSettings();
// Register against both faces: HUD bridges resolve the narrow
// provider, panel hosts resolve the UI Toolkit service.
FoundryServiceRegistry.Register<IZOAThemeService>(service);
FoundryServiceRegistry.Register<IHudThemeProvider>(service);
// Route ApplyZOATheme at the same instance instead of the
// in-process fallback.
VisualElementExtensions.SetThemeServiceResolver(
() => FoundryServiceRegistry.TryResolve<IZOAThemeService>(out var s) ? s : null);
}
}using UnityEngine.UIElements;
using ZOA.UI.Theming;
VisualElement BuildThemeSection(IZOAThemeService themeService)
{
return ZOAUiThemeSelectorWidget.Build(new ZOAUiThemeSelectorWidget.Options
{
Themes = ZOAUiThemeRuntimeSelection.LoadAvailableThemes(),
ActiveTheme = themeService.ActiveTheme,
ThemeService = themeService,
PersistSelection = true, // writes ZOA.UI.Theme.ActiveThemeId
WriteThemeSettings = false, // leave the project default alone
ApplyOnSelection = true,
ThemeSelected = theme => Debug.Log("Theme now " + theme.DisplayName),
});
}Integration
Behavior to account for
See also