UI themes: inheritance and local overrides
A working shared theme, lookup order and an isolated StyleBox for one button.
Store appearance in one place
Theme groups colors, constants, fonts, font sizes, textures, StyleBoxes and sounds by item name and Control type. For example, font_color for Button and font_color for Label are separate entries. Names and case must match the control API. Its API page lists available theme items.
Assign a shared theme to the UI root. Child Controls and Windows can use it and add their own themes. A local Theme Override suits one exception; a named type variation suits a reusable button variant. Do not copy the whole theme for one color.
Run the example
Add UiSampleStyle.cs and ThemeBasicsDemo.cs to a C# project. Attach ThemeBasicsDemo to a separate level's Control root, build and run with F6.
Changing the shared background changes the first two buttons' StyleBox. Separate first background copies the resource and changes only the first. Removing the override restores shared styling. Move the pointer away to inspect normal: hover and pressed have separate entries.
ThemeBasicsDemo.cs
using Reckfell;
public partial class ThemeBasicsDemo : Control
{
public Button Left { get; private set; } = null!;
public Button Right { get; private set; } = null!;
public StyleBoxFlat SharedNormal { get; private set; } = null!;
public override void _Ready()
{
UiSampleStyle.Prepare(this);
Theme.SetColor("font_color", "Button", Colors.White);
SharedNormal = (StyleBoxFlat)Theme.GetStylebox("normal", "Button");
var margin = new MarginContainer(); AddChild(margin); margin.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect, margin: 24);
var column = new VBoxContainer(); margin.AddChild(column);
column.AddChild(new Label { Text = UiSampleStyle.Text("Общая тема и локальное изменение", "Shared theme and local override") });
Left = new Button { Text = UiSampleStyle.Text("Первая кнопка", "First button") };
Right = new Button { Text = UiSampleStyle.Text("Вторая кнопка", "Second button") };
column.AddChild(Left); column.AddChild(Right);
var shared = new Button { Text = UiSampleStyle.Text("Изменить общий фон", "Change shared background") }; column.AddChild(shared);
shared.Pressed += () => SharedNormal.BgColor = new Color(.08f, .16f, .35f);
var local = new Button { Text = UiSampleStyle.Text("Отдельный фон первой", "Separate first background") }; column.AddChild(local);
local.Pressed += SeparateLeft;
var reset = new Button { Text = UiSampleStyle.Text("Убрать локальное изменение", "Remove local override") }; column.AddChild(reset);
reset.Pressed += () => Left.RemoveThemeStyleboxOverride("normal");
}
public void SeparateLeft()
{
var own = (StyleBoxFlat)Left.GetThemeStylebox("normal").Duplicate();
own.BgColor = new Color(.12f, .28f, .7f);
Left.AddThemeStyleboxOverride("normal", own);
}
}Resolve a value
A normal Control query first checks its local override. It then searches branch themes from the nearest owner toward parents, considering variation/type dependencies within each theme. Project/default themes and fallbacks follow the branch.
The theme owner here is a Control or Window carrying Theme in the UI chain, separate from the Entity.Owner property used when saving a hierarchy. An ordinary Entity breaks inheritance: Control(Theme) → Entity → Button does not pass the theme to the button. Group UI with Control/Container, or explicitly assign Theme to the child UI root. Changing Entity.Owner does not restore inheritance.
This is nearest-theme-first lookup, not a global rule that a variation always overrides Button everywhere. A partial local theme can replace one setting and inherit others. Control.GetThemeColor/GetThemeStylebox return the value resolved for that control. Theme.GetColor/GetStylebox read the resource itself without walking Control ancestry.
| Change | Scope |
|---|---|
| AddThemeColorOverride on one button | That Control; RemoveThemeColorOverride removes the exception |
| SetColor on a shared Theme | Users of that entry unless a closer value overrides it |
| Mutate a shared StyleBoxFlat | Every control sharing the resource |
| Duplicate the StyleBox and override locally | The separate copy |
A shallow Duplicate suffices for the shown StyleBoxFlat. For resources containing mutable subresources, check what remains shared; deep duplication has its own resource rules. GetThemeStylebox does not make an independent copy.
Distinguish three diagnostic questions:
| Query | Meaning |
|---|---|
Theme.HasColor("font_color", "Button") | Whether this exact resource has the entry |
button.HasThemeColorOverride("font_color") | Whether this button has a local exception |
button.GetThemeColor("font_color") | The value resolved through theme lookup |
Tune in a practical order
Set typography, content margins and ordinary states first. Then define hover, pressed, disabled and visible focus. StyleBox borders/margins can change required size; check layout with long translations. Modulate changes the whole rendered result rather than one specific theme color.
BeginBulkThemeOverride/EndBulkThemeOverride can group notifications for several local changes. End the block in finally if exceptions are possible: an unfinished bulk block leaves notifications deferred. This is a flag rather than a nesting counter; do not nest these blocks. Remove obsolete overrides when moving to a shared theme or they continue hiding the new design. Project-wide options include gui/theme/custom and gui/theme/custom_font; these settings require the corresponding restart.