Quick answer: Higher Layer values render on top. Inspect every CanvasLayer in the scene; another one may have a higher layer number than yours. Use conventions (HUD=10, Modal=50, Debug=100) with gaps for insertion.
Here is how to fix Godot CanvasLayer not rendering above UI. You have a game with a HUD on CanvasLayer 1 and a modal popup that should appear over everything — you put it on CanvasLayer 2. The modal shows up underneath the HUD. You raise the modal to layer 5. Still below. You raise to layer 50. Still below. Some other CanvasLayer in your tree has a higher layer number than you realized, and Godot is sorting correctly — your assumption was wrong.
The Symptom
A CanvasLayer with a specific Layer value renders below another CanvasLayer that should be below it. Changing the Layer number does not help until you find a very high number, which suggests some hidden high-layer CanvasLayer is blocking.
Variants: popup renders above HUD but below Pause menu dimmer. Tooltips render behind buttons they annotate. Debug overlay renders under every menu.
What Causes This
Hidden high-layer CanvasLayer. Autoload singletons often contain CanvasLayers for global UI (debug HUDs, screen fade effects, analytics toasts). If one of these is on layer 100 and you thought nothing was above layer 10, your popup at layer 20 is under it.
Parent CanvasLayer overriding. Nested CanvasLayers do not stack; each one starts a new layer at the absolute value. A CanvasLayer inside another CanvasLayer renders at its own Layer value, not relative to the parent.
z_index confused with Layer. Control nodes have z_index for ordering within a CanvasLayer. Setting z_index to 100 on a Control does not override CanvasLayer ordering — it only reorders that Control within its own CanvasLayer.
Viewport node changing order. A SubViewport with its own renderer can produce content that composites in unexpected order with CanvasLayers.
The Fix
Step 1: Audit every CanvasLayer in the scene. In the Scene dock, search for “CanvasLayer” (filter by node type). Check every instance’s Layer property. Include autoloads:
# Print all CanvasLayers and their layer values at runtime
func _ready():
for node in get_tree().get_nodes_in_group("canvas_layers"):
print(node.get_path(), " layer=", node.layer)
# Or find them all:
_print_canvas_layers(get_tree().root)
func _print_canvas_layers(node: Node):
if node is CanvasLayer:
print(node.get_path(), " layer=", node.layer)
for child in node.get_children():
_print_canvas_layers(child)
Run this. Output lists every CanvasLayer. Now you know what the actual ordering is.
Step 2: Adopt a layer convention. Document layer numbers across your project:
- World overlay: 0 (default)
- HUD: 10
- Panels / inventory: 20
- Popups: 50
- Tooltips: 90
- Modal overlays / screen transitions: 100
- Debug / developer UI: 200
Gaps let you insert a layer later (e.g. layer 75 for a tooltip above popups but below the overlay fade) without renumbering. Document in CLAUDE.md / README so team members do not pick random numbers.
Step 3: Use Popup nodes for modals. Godot 4 has built-in Popup and AcceptDialog nodes that handle z-ordering automatically via the Window system. Instead of a custom CanvasLayer popup, use:
func show_confirm_dialog():
var dialog = ConfirmationDialog.new()
dialog.dialog_text = "Are you sure?"
dialog.confirmed.connect(_on_confirmed)
add_child(dialog)
dialog.popup_centered()
Popup nodes render above all non-popup UI by design. No layer math needed.
Step 4: Diagnose Control ordering within a layer. For ordering within a CanvasLayer (e.g. which Control in a Panel renders on top):
- Higher-numbered children in the scene tree render on top
z_indexproperty overrides tree order- Move a Control up/down in the tree to reorder visually
Tooltip Z-Ordering
Tooltips deserve their own high-layer CanvasLayer. A hovered button at layer 10 spawns a tooltip at layer 90. This means tooltips always appear above panels. Combined with mouse_filter = IGNORE so the tooltip does not block clicks on the button.
Performance Note
Each CanvasLayer is a draw batch boundary — Godot flushes the current batch before rendering the next layer. Hundreds of CanvasLayers cost performance. Use layers semantically (HUD, Menu, Tooltip, Debug), not as a per-element z-sort tool.
“Layer numbers are a global contract. Pick them intentionally, document them, and inspect the whole scene when order feels wrong.”
Related Issues
For SubViewport issues, see Godot SubViewport Texture Blank. For signal issues affecting UI, Godot Await Signal Never Completing covers related patterns.
Audit layers at runtime. Pick a convention with gaps. Popup nodes for modals.