ボタンの色を少し変えたいだけなのに、シーンごとに置いた何十個ものボタンを1つずつ直す——UIのスタイルを各ノードで個別に設定していると、こんな終わりのない修正地獄にはまります。しかもシーンごとに微妙に見た目がズレて、なんとなく素人っぽいUIになりがちです。
これを解決するのが、Godotの テーマ(Theme)システム です。色・フォント・余白・ボタンのスタイル を 1つのデザインシステム としてまとめて管理し、テーマを差し替えるだけでUI全体の見た目を一斉に変えられます。ダークモード切り替えのような機能も、これで一気に現実的になります。
この記事でわかること
Themeを構成する5つのアイテム(Color / Constant / Font / Icon / StyleBox)- テーマが上から下へ流れる 継承 の仕組みと優先順位
StyleBoxFlatでボタンの normal / hover / pressed を作る方法preloadしたテーマを差し替える ダークモード切り替え の実装
テーマを構成する5つのアイテム
GodotのUIはすべて Control から派生していて、その見た目は Theme リソースが決めます。Theme は、スタイルを定義する アイテム の集まりです。

| アイテム | 何を決めるか | 例 |
|---|---|---|
| Color | 色(文字色・背景色など) | ボタンの文字色、ラベル色 |
| Constant | 数値(余白・間隔など) | コントロール間のスペース |
| Font | フォント(種類・サイズ) | 標準フォント、見出しフォント |
| Icon | アイコン画像 | チェックボックスのチェック |
| StyleBox | 最重要。 背景・枠・角丸・影 | ボタンやパネルの見た目そのもの |
これらは「どのノードタイプ(例: Button)の、どのアイテム名(例: font_color・normal)か」の組み合わせで管理されます。中でも StyleBox がデザインの大部分を担う心臓部です。
テーマの継承と優先順位
テーマは、シーンツリーを 上から下へ滝のように流れます(CSSに似ています)。同じ要素に複数の指定があるときは、次の順で優先されます。

- ノード固有のオーバーライド(
add_theme_*_override())← 最優先 - そのノードの
theme - 親から継承した
theme - プロジェクト設定のデフォルトテーマ
- Godotエディタのデフォルト(最後のフォールバック)
理想は、 プロジェクト全体を1つのグローバルテーマで統一し、例外だけを下位で上書きする 流れです。これで「9割はテーマ、1割だけ個別調整」というきれいな設計になります。
StyleBoxFlatでボタンをデザインする
まずはコードなしで、テーマエディタを触ってみましょう。ファイルシステムで右クリック →「新規リソース」→ Theme を作り、main_theme.tres として保存します。これを [プロジェクト設定] → [Gui] → [Theme] → [Custom] に割り当てると、プロジェクト全体のデフォルトテーマになります。
テーマエディタで最も多用するのが StyleBoxFlat です。これ1つで背景色・角丸・枠線・影を設定でき、フラットデザインからマテリアル風まで作れます。

Button タイプを追加し、Styles の normal(通常時)に「新規 StyleBoxFlat」を作って、次を設定します。
- Bg Color :背景色
- Corner Radius :四隅の角丸
- Border Width / Color :枠線の太さと色
- Shadow :影(立体感を出す)
同じ要領で hover(少し明るく)、pressed(少し暗く)、disabled(薄く)も作ると、状態の変化が自然になります。これで、プロジェクト内の すべての Button が自動的にこのスタイルに揃います 。
実践:ダークモード切り替えを作る
テーマの真価は、コードから丸ごと差し替え られることです。多くのゲームや設定画面にある ダークモード / ライトモード切り替え を作ってみましょう。RPGでもツール系でも、やることは同じです。
light_theme.tres と dark_theme.tres の2つを用意し、切り替えを Autoload のシングルトンに持たせます。

# settings.gd (Autoloadに登録)
extends Node
const LIGHT_THEME := preload("res://themes/light_theme.tres")
const DARK_THEME := preload("res://themes/dark_theme.tres")
var is_dark := false:
set(value):
is_dark = value
_apply_theme()
func _ready() -> void:
_apply_theme()
func _apply_theme() -> void:
# ルート(ビューポート)にテーマを入れると、UI全体へ流れる
get_tree().root.theme = DARK_THEME if is_dark else LIGHT_THEME
func toggle() -> void:
is_dark = not is_dark
設定画面のボタンから Settings.toggle() を呼ぶだけで、ゲーム全体のUIが一瞬で切り替わります。ボタンとこの処理をつなぐのは シグナル の出番です。
特定のボタンだけを一時的に目立たせたいときは、テーマ自体はいじらず、そのノードだけを上書きします。ただし 共有リソースを直接書き換えないよう、必ず .duplicate() してから変更しま す。
func highlight(button: Button) -> void:
# 共有StyleBoxをそのまま変えると全ボタンに波及する → 複製して変える
var box := button.get_theme_stylebox("normal").duplicate() as StyleBoxFlat
box.border_width_bottom = 8
button.add_theme_stylebox_override("normal", box)
button.add_theme_font_size_override("font_size", 24)
ポイントは2つです。
- 全体はテーマ差し替えで一括 :
get_tree().root.themeを入れ替えれば、UI全体が一斉に切り替わります。1ノードずつ直す必要はありません。 - 例外だけ上書き、必ず複製 :
get_theme_stylebox()の戻り値は共有物です。直接いじると全ボタンに影響するので、.duplicate()した複製を上書きするのが鉄則です。
よくある間違いとベストプラクティス

| よくある間違い | ベストプラクティス |
|---|---|
何でも add_theme_*_override() で個別上書きし、継承を無視する | ベーステーマを定義し、差分だけを下位で上書きする |
| すべてのスタイルを1つの巨大テーマに詰める | 「ベース」「ゲームUI」「メニュー」など関心事で 分割 する |
get_theme_stylebox() の戻り値を直接変更する | 必ず .duplicate() してから変更する(共有物への副作用を防ぐ) |
| テーマを使わず全ノードを個別設定する | テーマでUIの9割を定義し、例外だけオーバーライド |
おまけ:先に知っておくと良いこと
- カスタムControlをテーマ対応に :自作UIノードは
_notification()でNOTIFICATION_THEME_CHANGEDを受け、get_theme_color()などで取り直してqueue_redraw()すると、テーマ変更に追従します。 - テーマは分割して組み合わせる :ベーステーマの上に、メニュー用・ゲーム内HUD用のテーマを重ねると管理しやすくなります。
- 切り替えは preload が有利 :ダーク/ライトのように切り替えるテーマは
preloadしておくと、代入するだけで軽快に切り替わります。 - 土台はコンテナ :見た目の前に、配置は Controlノードとレイアウトコンテナ で整えておくと、テーマの効果が活きます。
まとめ
Theme:Color / Constant / Font / Icon / StyleBox の5アイテ ムでUIの見た目を一元管理する- 継承 :オーバーライド > ノードのtheme > 親 > プロジェクト > エディタ の順。全体を統一し例外だけ上書き
StyleBoxFlat:背景・角丸・枠・影でボタンなどをデザイン。状態(normal / hover / pressed)ごとに用意する- 動的切り替え :
preloadしたテーマをget_tree().root.themeに代入すればダークモードも一発。個別上書きは.duplicate()してから
まずはテーマエディタで StyleBoxFlat を1つ作り、Button の見た目を変えるところから始めてみてください。それだけで、UI全体の統一感がぐっと上がります。配置の土台は Controlノードとレイアウトコンテナ と合わせて押さえると、UI作りの基礎が固まります。