A hard cut between scenes feels cheap. The screen just blinks and you're somewhere else. A half-second fade to black and back makes the same scene change feel deliberate and polished. It's one of those tiny touches that quietly signals "this was made with care." Here's how to build a transition you can call from anywhere.
The plan
You want one transition system the whole game shares, not a copy pasted into every scene. So we'll build an autoload: a persistent script that owns a black overlay, fades it in, swaps the scene while the screen is black, then fades it back out. One function call and the whole dance happens.
Build the overlay
Make a new scene with this structure:
CanvasLayer(root) so the overlay always draws on top of everythingColorRectnamedFade, color set to solid black, anchored to fill the full screen
Set the ColorRect's alpha to 0 to start, so it's invisible. Save the scene as scene_transition.tscn.
The transition script
Attach this script to the CanvasLayer root:
extends CanvasLayer@onready var fade: ColorRect = $Fadefunc _ready() -> void: # Keep running even if the game is paused process_mode = Node.PROCESS_MODE_ALWAYS # Start fully transparent and let clicks pass through fade.modulate.a = 0.0 fade.mouse_filter = Control.MOUSE_FILTER_IGNOREfunc change_scene(path: String, duration: float = 0.3) -> void: # Fade to black fade.mouse_filter = Control.MOUSE_FILTER_STOP var tween := create_tween() tween.tween_property(fade, "modulate:a", 1.0, duration) await tween.finished # Swap the scene while the screen is black get_tree().change_scene_to_file(path) # Fade back in tween = create_tween() tween.tween_property(fade, "modulate:a", 0.0, duration) await tween.finished fade.mouse_filter = Control.MOUSE_FILTER_IGNOREAdd this scene as an autoload named SceneTransition in Project Settings.
Use it from anywhere
Now changing scenes with a fade is one line, from any script in the game:
func _on_play_pressed() -> void: SceneTransition.change_scene("res://scenes/Level.tscn")The overlay fades up, the scene swaps behind the black, and it fades back to reveal the new scene. That await on the tween's finished signal is what lets the steps wait for each other, and it comes straight from how tweens and signals work together.
A couple of details that matter
process_mode = PROCESS_MODE_ALWAYS is important. If you trigger a transition from a pause menu while the game is paused, the fade still needs to run, and this lets it. The mouse_filter toggling stops players from clicking buttons through the black during the fade, then lets input through again once it's clear.
Want a fancier wipe instead of a plain fade? Swap the ColorRect for a TextureRect with a shader, or animate a different property. The structure stays the same. The autoload owns it, you call one function, the transition just happens.
Tie it together
Scene transitions are the glue between your menus, levels, and game-over screens. Once this autoload exists, every scene change in your game can use it for free, and the whole thing feels a notch more finished. If you're still building out the scenes it connects, the free Inventory System quest gives you real systems and screens to wire transitions between.
FAQ
How do I fade between scenes in Godot 4?
Build an autoload with a CanvasLayer and a full-screen black ColorRect, tween its alpha to 1 to fade out, call change_scene_to_file() while the screen is black, then tween the alpha back to 0. Awaiting each tween's finished signal sequences the steps.
Why should the scene transition be an autoload?
So the whole game shares one transition instead of duplicating it in every scene. An autoload persists across scene changes, which means the overlay can keep fading in even as the scene behind it swaps, and you can trigger it from a single function call anywhere.
How do I keep a transition working while the game is paused?
Set the transition node's process_mode to PROCESS_MODE_ALWAYS. Otherwise a transition triggered from a pause menu won't animate, because the node is paused along with the rest of the game. Always is the mode that keeps it running regardless of pause state.