A hard cut between scenes looks like a glitch. A half-second fade makes the same game feel finished. This guide builds Unity scene transitions the robust way: a persistent fade canvas that survives scene loads, a fade to black combined with async loading, wipe and circle transitions, and the fix for the annoying one-frame flash of the old or new scene that most tutorials leave in.
How a Scene Transition Works
Every transition follows the same four steps:
- Cover the screen: fade a black image to full opacity, or wipe it across.
- Load the next scene while the screen is covered.
- Wait until the new scene has actually rendered a frame.
- Reveal the new scene by fading the image back out.
The covering object must survive the load. If it lives in the old scene, it gets destroyed halfway through and you see a pop. So the fader lives on a canvas marked DontDestroyOnLoad, created once, and reused for every transition in the game.
Building the Persistent Fade Canvas
- Create a Canvas named
SceneFader. Render Mode: Screen Space – Overlay. Set Sort Order to something high like 999 so it draws on top of every other UI. - Add a child Image, stretch its anchors to fill the screen (Alt-click the stretch preset), and set its colour to black.
- Add a CanvasGroup to the Canvas. You’ll animate its
alpha, and useblocksRaycaststo stop clicks reaching buttons mid-transition. - Make the whole thing a prefab and put it in a
Resourcesfolder, so code can spawn it from any scene.
The fader canvas doesn’t need its own EventSystem. Don’t add one, or you’ll get “multiple EventSystems” warnings once it persists alongside the one in each scene.
The SceneFader Script
This version spawns itself automatically the first time it’s needed, so you never have to remember to place it in a scene:
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
[RequireComponent(typeof(CanvasGroup))]
public class SceneFader : MonoBehaviour
{
static SceneFader instance;
public static SceneFader Instance
{
get
{
if (instance == null)
instance = Instantiate(Resources.Load<SceneFader>("SceneFader"));
return instance;
}
}
[SerializeField] float fadeDuration = 0.4f;
CanvasGroup group;
bool busy;
void Awake()
{
if (instance != null && instance != this) { Destroy(gameObject); return; }
instance = this;
DontDestroyOnLoad(gameObject);
group = GetComponent<CanvasGroup>();
group.alpha = 0f;
group.blocksRaycasts = false;
}
public void LoadScene(string sceneName)
{
if (!busy) StartCoroutine(Transition(sceneName));
}
IEnumerator Fade(float target)
{
float start = group.alpha, t = 0f;
while (t < fadeDuration)
{
t += Time.unscaledDeltaTime;
group.alpha = Mathf.Lerp(start, target, t / fadeDuration);
yield return null;
}
group.alpha = target;
}
IEnumerator Transition(string sceneName)
{
busy = true;
group.blocksRaycasts = true;
yield return Fade(1f);
yield return SceneManager.LoadSceneAsync(sceneName);
yield return null; // let the new scene render one frame
yield return Fade(0f);
group.blocksRaycasts = false;
busy = false;
}
}
Call it from anywhere with SceneFader.Instance.LoadScene("Level2");. Two details matter:
Time.unscaledDeltaTime: if you transition from a pause menu whereTime.timeScaleis 0, a scaled fade never finishes. Remember to resettimeScaleto 1 before or after loading.- The
busyflag stops a double-click from starting two overlapping transitions, which is a classic source of scenes loading twice.
Because the fader runs coroutines on a persistent object, they keep running through the load. A coroutine on an object in the old scene would stop the moment that scene unloads. If you prefer async/await, the same logic works with Awaitable; our Awaitable vs coroutines guide shows the conversion.
Fading With Async Loading
LoadSceneAsync already avoids freezing the fade itself, but for big scenes you may want to show progress, or hold on the black screen until loading finishes. Use allowSceneActivation:
IEnumerator Transition(string sceneName)
{
busy = true;
group.blocksRaycasts = true;
yield return Fade(1f);
AsyncOperation op = SceneManager.LoadSceneAsync(sceneName);
op.allowSceneActivation = false;
float shownAt = Time.unscaledTime;
while (op.progress < 0.9f)
{
// progressBar.value = op.progress / 0.9f;
yield return null;
}
// Optional: keep the loading screen up for a minimum time so it doesn't flicker.
while (Time.unscaledTime - shownAt < 0.3f) yield return null;
op.allowSceneActivation = true;
while (!op.isDone) yield return null;
yield return null;
yield return Fade(0f);
group.blocksRaycasts = false;
busy = false;
}
Progress stops at 0.9 while allowSceneActivation is false; the last 10% is activation, which only happens once you allow it. That’s expected, not a bug. For a full loading screen with tips and a progress bar, see our async loading screen tutorial. Scenes loaded through Addressables follow the same pattern using the Addressables scene load handle.
Avoiding the Flash of the Old Scene
The most common complaint about fades is a single-frame flash. There are three different causes:
The new scene flashes before the fade-in
Activation finished, but the black image went transparent in the same frame the new scene’s first frame rendered, sometimes before objects initialised in Start. The yield return null after loading fixes this by guaranteeing one fully rendered frame under the black cover. If your scene does heavy setup in Start (spawning a level, building a navmesh), wait two frames or wait for your own “level ready” event.
The old scene flashes after the fade-out
The fader isn’t actually persisting. It’s being destroyed with the old scene and recreated in the new one at alpha 0. Check the Hierarchy during play: a working fader appears under the DontDestroyOnLoad heading. Also make sure there isn’t a second SceneFader placed in the new scene that sets alpha to 0 in its Awake; the singleton guard above destroys duplicates before they can.
The first scene pops in at game start
If you want a fade-in when the game launches, start the fader at alpha 1 and fade to 0 from Start. Put the splash or menu camera’s background colour to black too, so the first frame before any UI renders is already black.
Wipe and Circle Transitions
Once the structure is in place, swapping the effect is easy, because only the Fade coroutine changes.
Horizontal wipe with a filled Image
Set the black Image’s Image Type to Filled, Fill Method Horizontal, Fill Origin Left. A sprite is required for Filled images, so assign Unity’s built-in white “UISprite” or any white square. Then animate fillAmount instead of alpha:
[SerializeField] UnityEngine.UI.Image cover;
IEnumerator Wipe(float target)
{
group.alpha = 1f;
float start = cover.fillAmount, t = 0f;
while (t < fadeDuration)
{
t += Time.unscaledDeltaTime;
float k = Mathf.SmoothStep(0f, 1f, t / fadeDuration);
cover.fillAmount = Mathf.Lerp(start, target, k);
yield return null;
}
cover.fillAmount = target;
}
For a wipe that continues in the same direction instead of reversing, flip fillOrigin between the cover and reveal phases. Radial 360 gives a clock wipe with no other changes.
Circle (iris) transition
The classic shrinking circle needs a tiny shader. In Shader Graph, create a Canvas shader graph (Unity 6 supports Canvas as a target for uGUI), compute the distance from the UV to the centre (correcting for aspect ratio), and output alpha with a Step or Smoothstep against a _Radius float property. Assign the material to the cover Image and animate the property:
static readonly int RadiusId = Shader.PropertyToID("_Radius");
IEnumerator Iris(float from, float to)
{
Material m = cover.material; // instance created on first access
float t = 0f;
while (t < fadeDuration)
{
t += Time.unscaledDeltaTime;
m.SetFloat(RadiusId, Mathf.Lerp(from, to, t / fadeDuration));
yield return null;
}
m.SetFloat(RadiusId, to);
}
Pass the player’s screen position as a second property to centre the iris on them, Mario-style. Use Camera.main.WorldToViewportPoint to get it in 0 to 1 coordinates.
Other ideas
- Dissolve: a noise texture compared against a threshold in the same Canvas shader.
- Colour fades: fade to white for a flash-bang or death transition by changing the Image colour.
- Audio: fade the music mixer group with the screen. A visual fade with an audio cut still feels abrupt.
🎬 Want 50 transitions without writing shaders?
Transition packs on the Asset Store give you dozens of wipes, irises and dissolves that plug into a script like this one.
Troubleshooting
The fade never finishes or stops halfway
Either Time.timeScale is 0 and you used Time.deltaTime, or the coroutine was running on an object that got destroyed by the scene load. Use unscaled time and run it on the persistent fader.
Buttons in the new scene don’t work after a transition
blocksRaycasts was left true, so the invisible fader eats every click. Make sure it’s set false at the end of every path, including when a load fails.
The fader appears behind my UI
Raise the fader Canvas Sort Order above every other canvas. Screen Space – Camera canvases with a higher plane distance can also sort oddly; Overlay is simplest.
The game loads the scene twice
Two calls started two transitions. Guard with a busy flag, and disable the button that triggered the load.
Progress stays at 0.9 forever
That’s what happens with allowSceneActivation false. Set it to true when you’re ready and wait for isDone.
FAQ
Do I need DontDestroyOnLoad for a fade?
Yes, or an additive “persistent” scene. The cover must exist during the load, so it can’t belong to the scene being unloaded.
How long should a scene fade be?
0.3 to 0.6 seconds each way feels good for most games. Longer fades start to feel sluggish when players die and retry often.
Can I fade with the Animator instead of code?
Yes. Animate the CanvasGroup alpha with Fade In and Fade Out clips set to Unscaled Time update mode, and trigger the load from an animation event or after the clip length.
Does this work with additive scene loading?
Yes. Replace LoadSceneAsync(name) with LoadSceneAsync(name, LoadSceneMode.Additive) and unload the previous scene while the screen is covered.
Why use LoadSceneAsync if the screen is black anyway?
A synchronous LoadScene freezes the main thread, so any loading animation or audio fade stops. Async keeps the game responsive under the cover.
Reader questions
Asked by readers of this article and answered by AI. Behavior can differ between Unity versions, so test in yours.




Leave a Reply