Theme question: Best way to have a "base" theme and then dynamically apply minor theme changes?

Godot Version

4.4

Background

I am creating a Sudoku UI, which is a game with a 9x9 grid where each square in the grid can show a number from 1-9.

Presently, I have:

  1. A Square scene, which represents a single square in the grid and is composed of a Panel with a Label child.
  2. A Row scene, which is an HBoxContainer that contains nine Square scenes as children.
  3. A Rows scene, which is a VBoxContainer that contains nine Row scenes.

I have code attached to the Square scene that sets the Label text based on the assigned value to that square (if any), and code in the Rows scene that works with the grid as a whole.

Question

In one sentence: What is the recommended best practices for UI elements that have a “base” theme but need to override certain parts of it based on the element’s position or user input?

What’s a good way to theme the Square scene’s Panel and Label given that the styling needs to change frequently based on position or user interaction?

For example, by default all squares should have a white background and a thin, light gray border. BUT… if the square is on the edge of the grid then it should have a thicker, black background along the edge. Likewise, if the square makes up one of the edges of the 3x3 boxes in Sudoku then it should have the same thicker, black background along the box edge.

I see that with a theme I can define the Panel’s border color and width for all four sides, but how would I go about saying, "For these square use the same default border color and width for its top, left, and right border, but override the bottom border styling to have a thicker, black border?

Do I need to create a unique theme for each combination of border differences? Or can I override these programmatically in a loop?

You don’t have to create an entirely new theme, you can just use the theme overrides that control nodes have. You can assign a different stylebox to the panel that needs to have its background changed, and remove it to revert back to the theme’s stylebox.

Will that work on a property-by-property basis? For example, there can be the following border combinations:

  1. Light border on all four sides
  2. Dark border on top, light border on all other sides
  3. Dark border on right, light border on all other sides
  4. Dark border on bottom, light border on all other sides
  5. Dark border on left, light border on all other sides
  6. Dark border on top and right, light border on all other sides
  7. Dark border on top and left, light border on all other sides
  8. Dark border on bottom and right, light border on all other sides
  9. Dark border on bottom and left, light border on all other sides

Do I need to create one theme for scenario (1) and then eight separate style boxes for scenarios 2…9?

Or can I create four styles boxes, like TopDarkBorder, RightDarkBorder, BottomDarkBorder, and LeftDarkBorder, and then combine them as needed? E.g., for scenario (9) it would use the default theme and then apply overrides BottomDarkBorder and LeftDarkBorder?

What’s the best practice if you have some base styling, but then need to make changes to specific style properties dynamically?

do not use theme overrides.
you need a single theme on the first Control node, all children will inherit the theme.
then you want to create a theme variation for each element that has to be different, like a button of a different color.
you put the name of your type variation on the node’s theme_type_variation and it will change it.
all other nodes will use the style defined by your theme, if you add a Button, all Buttons will use this style, the button you want to be different you set a theme_type_variation.

you can change the theme_type_variation by code directly and it will change in game, it uses a String.

use theme_type_variation s, change the property from code.

don’t use a panel, use a Button. it has it’s own text and reacts to clicking and hovering. then create new buttons instead of using a scene.
in theme you can set highlights and button press effects.

you can also create a new script, inherit Button, set a class_name, and add methods for mouse enter and mouse exit with a tween. connect the methods to the signals in ready.
then you can create a new button from that, by using the custom name, and it will instantiate a “button with the script”.

my_button.gd

class_name MyButton
extends Button

func _init( txt : String) -> void:#we can set custom arguments for use with new
	text = txt

func _ready() -> void:
	mouse_entered.connect(on_mouse_entered)
	mouse_exited.connect(on_mouse_exited)

func on_mouse_entered() -> void:
	var tween : Tween = create_tween()
	tween.parallel().tween_property(self, "modulate", Color(0.2, 1.0, 0.2, 1), 0.1)

func on_mouse_exited() -> void:
	var tween : Tween = create_tween()
	tween.parallel().tween_property(self, "modulate", Color(1, 1, 1, 0), 0.3)

var n_button : MyButton = MyButton.new("9")

again, do not use overrides from the inspector, do everything from a single theme and use variations.
what you don’t change in a theme variation will remain the same as the base.
as for styleboxes, you can save them to a file and quickload them in the variations. you can also duplicate a resource in files and change it to speed things up more.

@jesusemora - if I use theme variants, how do I handle situations where multiple variations may apply to a single Panel (or Button)?

For example, a user can “select” a square in the grid, in which case it should have a border drawn around that square. The user can also highlight all squares with a specific value.

So a square could have four states:

. Selected (YES) Selected (NO)
Highlighted (YES) Border and yellow background No border and yellow background
Highlighted (NO) Border with white background No border and white background

Does that mean I need to create four theme variants to handle these four cases? There will be another one or two dimensions, meaning the number of variations will grow geometrically.

I come from a web background. In CSS you can say define styling vary granularly, like, “All elements with class highlighed should have a yellow background and all elements with class selected should have a border,” and then you can specify at each element whether it has class highlighted, selected, both, or neither. Is there an analogue in Godot?

when you define a theme variation, and you set a style, that style is overriding the Base theme for that element. so you can choose what elements to change.

this next thing is a bit tricky: a variation can inherit another variation. but you must first define the same base type for both, then change the base type on one to the other, otherwise it will not update and show styles.
for example, we create a variation of Button called red and set a Normal property of styleboxflat with red color. then create a variation of Button called red_with_highlight and set a hover property of styleboxflat with yellow color.
then change red_with_hightlight Base to red.
using red will make the button red, using red_with_highligh will make it red but also change to yellow when the mouse is over it.

a button can have a toggle mode, in that case you can use the pressed state to have the border. there is also a pressed highlight. so this would not be 4 variations but just 2.

well I would create the variations, but you can also combine more than one Node or override draw like you originally thought.
the problem is the border, because in godot this is drawn as part of the StyleBoxFlat or within the texture of StyleBoxTexture.

one thing you CAN do is use the modulate property of the node to colorize the button, this could help with some situations.