UI Toolkit: Theme Switcher


Runtime theme switching for Unity UI Toolkit. Light/dark/custom themes, token overrides, OS dark-mode auto-follow on Windows + macOS, smooth crossfades, persistence. 3 demos. Zero dependencies.


by KrookedLilly


Price History +

Unity 6 ships TSS (Theme Style Sheets) and USS custom properties as primitives — but no turnkey runtime theme system. There's no C# swap API for driving themes from code; no OS dark-mode detection; every swap is a hard cut with no transition; and no way to override a single token at runtime.


Drop a ThemeManager onto your UIDocument GameObject, point it at a ThemeRegistry, and themeManager.SetTheme("dark") swaps the entire panel — or call themeManager.SetToken("--color-accent", Color.red) to override a single CSS variable at runtime without touching files. The full theme-swap pipeline is awaitable, the pre-change event is cancellable for guard logic, and the override layer persists through theme swaps so user preferences survive.


Async swap API that feels native.

await themeManager.SetThemeAsync("dark") completes when the swap finishes — so your code reads top-to-bottom. Cancellation tokens, BeforeThemeChange with a Cancel flag for guard logic, ThemeChanged for reactive UI, and ThemeTransitionComplete for post-swap hooks. Synchronous SetTheme(id, skipTransition) for explicit instant swaps. NextTheme() / PreviousTheme() cycle the registry. PushTheme("preview") / PopTheme() for settings screens where the user samples a theme then commits or backs out.


Three swap strategies, picked per theme.

StyleSheetSwap (default) adds and removes USS files on the root. ThemeStyleSheetSwap assigns panelSettings.themeStyleSheet for global, cross-document themes (with a warning when the PanelSettings is shared across UIDocuments). RootVariableOverride keeps your single base USS and treats themes as pure token state. Set the strategy field on the ThemeDefinition asset and the right swap path runs automatically.


Token overrides with one-line element bindings.

SetToken("--color-accent", Color.red) stores a runtime override that outlives every subsequent SetTheme call. To reflect the value on a specific element, wire it once with BindColorToken:

themeManager.BindColorToken(myButton, "--color-accent",
(el, c) => el.style.backgroundColor = c);

The binding applies the current value immediately, re-applies on every override or theme change, returns an IDisposable for explicit cleanup, and auto-unsubscribes when the element detaches from its panel. BindFloatToken ships the same surface for numeric tokens. For multi-token or derived-value scenarios, drop down to the raw TokenChanged event.

TryGetToken reads with override → theme → parent-chain precedence. ClearTokenOverride / ClearAllTokenOverrides remove. Color and float types both supported.


Theme inheritance.

Declare a base theme with shared tokens and a dark variant that only overrides what changes. The resolver walks the parent chain (cycle-detected, max depth 16) for every token read. Stop repeating yourself across every theme variant.


Lifecycle hooks for every swap.

Three events fire in order on every theme swap: BeforeThemeChange (cancellable — set ctx.Cancel = true to abort), ThemeChanged (immediately after stylesheets and overrides apply), and ThemeTransitionComplete (after the registered animator finishes, or immediately when no animator is registered). Per-manager transition duration; skip-transition flag on every swap call when you need an explicit instant cut.


Persistence built in.

PlayerPrefsThemeStorage (configurable key) is the default — your user's choice persists across runs without any extra setup. InMemoryThemeStorage for no-persist mode. A Custom mode and the IThemeStorage interface let you wire Cloud Save, a JSON file, or an encrypted backend with a single property assignment.


OS dark-mode auto-follow.

Windows and macOS providers ship out of the box. themeManager.SystemProvider = new WindowsThemeSystemProvider() (or MacOSThemeSystemProvider) plus themeManager.Mode = ThemeMode.Auto and your panel automatically follows the user's OS-level light/dark preference. Both providers poll for changes (2s default), clean up cleanly when disposed, and degrade to manual mode on unsupported platforms. Implement IThemeSystemProvider for iOS, Android, Linux, or any other platform.


Animated crossfades via UI Toolkit: Tween Engine.

For smooth color crossfades between themes, install UI Toolkit: Tween Engine and enable the Tween Engine to Theme Switcher integration from Tools > KrookedLilly > Setup (then click Apply Changes). The integration registers a token interpolator that runs every time you call SetTheme / SetThemeAsync, writing per-frame interpolated values into the override store for elements wired via BindColorToken. Mid-flight swaps cancel cleanly and redirect smoothly toward the new target — clicking Toggle three times in a row produces a continuous animation, not a stutter. Without Tween Engine, theme swaps complete instantly.


Locale-aware root classes.

SetLocale("en", rtl: false) writes locale-en and locale-rtl / locale-ltr classes to the document root. Author per-locale USS rules with zero code branches: .locale-ja .demo-title { font-size: 18px; }, .locale-rtl .nav-row { flex-direction: row-reverse; }. Subscribe to Unity Localization's SelectedLocaleChanged event and forward the new locale code to SetLocale — full recipe in the included docs.


Three demo scenes included.

Basic Toggle — Toggle, Next, Previous buttons walk three sample themes (light, dark, high-contrast) with a live "Active theme: X" label; install Tween Engine to see the visible crossfade between themes. Palette Token — three RGB sliders drive an override into --color-accent; watch the swatch and accent-styled buttons update live as you drag; "Clear Override" restores the theme's accent; "Toggle Theme" proves the override outlives the swap. System Theme — "Follow OS theme" toggle binds the Windows or macOS provider to the manager and flips between Manual and Auto; readout labels show the detected OS preference and the active theme. All three ship with controller scripts, UXML, USS, sample Light + Dark + High Contrast themes, and a configured DemoPanelSettings (Scale With Screen Size, 1920×1080, Match=0).


Full C# source, no DLLs.

XML documentation on every public API. Three custom inspectors (manager + registry + theme definition), an editor window for live theme preview against the active manager, and a quick-create that stamps out a Light + Dark + Registry triplet ready to fill in. Works with both Unity's Legacy Input Manager and the new Input System package. Zero external dependencies.