Tutorial · Game systems

How to make a custom loading screen in Roblox Studio

A custom loading screen is a ScreenGui that a LocalScript in ReplicatedFirst shows the moment a player joins, before the rest of your game has arrived. This guide builds one in code: your title, a progress bar while images and sounds preload, a Skip button for slow connections, and a fade into the game.

Updated 21 October 20266 min readBeginnerLuau

In short

Put a LocalScript in ReplicatedFirst. It runs before anything else reaches the player. In it, create a ScreenGui with IgnoreGuiInset on, parent it to the player's PlayerGui, and call ReplicatedFirst:RemoveDefaultLoadingScreen(). Then wait for game.Loaded, preload what matters with ContentProvider:PreloadAsync(), and destroy the ScreenGui when you are done.

  • Only a LocalScript in ReplicatedFirst runs early enough. In StarterGui or StarterPlayerScripts it starts after most of the game has loaded, so the screen appears too late.
  • Build the screen in code, or keep your designed ScreenGui inside the script. Anything in StarterGui has not arrived yet when the script starts.
  • Do not preload the whole game one object at a time. Preload in batches, or only the images and sounds the player sees first.

Get the script

Custom loading screen with a progress bar

One LocalScript in ReplicatedFirst replaces the default loading screen with your title, a progress bar, a Skip button and a fade out.

LocalScript in ReplicatedFirst named LoadingScreen
-- LocalScript in ReplicatedFirst named LoadingScreen.
-- Replaces Roblox's default loading screen with your own: a title, a progress bar,
-- a Skip button for slow connections, and a fade out when the game is ready.
local ContentProvider = game:GetService("ContentProvider")
local Players = game:GetService("Players")
local ReplicatedFirst = game:GetService("ReplicatedFirst")
local TweenService = game:GetService("TweenService")

local TITLE = "My Game"
local MIN_SECONDS = 2 -- show the screen at least this long, so it never just flashes
local SKIP_AFTER_SECONDS = 8 -- offer a Skip button when loading takes longer than this
local STEPS = 20 -- how many times the bar moves while assets preload

local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui") :: PlayerGui

-- The screen is built in code, so it exists before anything else in the game has loaded
local gui = Instance.new("ScreenGui")
gui.Name = "LoadingScreen"
gui.IgnoreGuiInset = true -- also cover the strip at the top of the screen
gui.DisplayOrder = 100 -- in front of every other ScreenGui
gui.ResetOnSpawn = false

local background = Instance.new("Frame")
background.Size = UDim2.fromScale(1, 1)
background.BackgroundColor3 = Color3.fromRGB(20, 16, 32)
background.BorderSizePixel = 0
background.Parent = gui

local title = Instance.new("TextLabel")
title.Text = TITLE
title.Font = Enum.Font.GothamBold
title.TextSize = 42
title.TextColor3 = Color3.new(1, 1, 1)
title.BackgroundTransparency = 1
title.AnchorPoint = Vector2.new(0.5, 0.5)
title.Position = UDim2.fromScale(0.5, 0.42)
title.Size = UDim2.fromOffset(600, 60)
title.Parent = background

local barBack = Instance.new("Frame")
barBack.AnchorPoint = Vector2.new(0.5, 0.5)
barBack.Position = UDim2.fromScale(0.5, 0.54)
barBack.Size = UDim2.fromOffset(320, 10)
barBack.BackgroundColor3 = Color3.fromRGB(60, 52, 84)
barBack.BorderSizePixel = 0
barBack.Parent = background

local barFill = Instance.new("Frame")
barFill.Size = UDim2.fromScale(0, 1)
barFill.BackgroundColor3 = Color3.fromRGB(160, 120, 255)
barFill.BorderSizePixel = 0
barFill.Parent = barBack

local status = Instance.new("TextLabel")
status.Text = "Loading..."
status.Font = Enum.Font.Gotham
status.TextSize = 18
status.TextColor3 = Color3.fromRGB(200, 190, 230)
status.BackgroundTransparency = 1
status.AnchorPoint = Vector2.new(0.5, 0)
status.Position = UDim2.fromScale(0.5, 0.57)
status.Size = UDim2.fromOffset(320, 24)
status.Parent = background

local skip = Instance.new("TextButton")
skip.Text = "Skip"
skip.Font = Enum.Font.GothamBold
skip.TextSize = 18
skip.TextColor3 = Color3.new(1, 1, 1)
skip.BackgroundColor3 = Color3.fromRGB(60, 52, 84)
skip.AnchorPoint = Vector2.new(0.5, 0)
skip.Position = UDim2.fromScale(0.5, 0.64)
skip.Size = UDim2.fromOffset(120, 36)
skip.Visible = false
skip.Parent = background

gui.Parent = playerGui
ReplicatedFirst:RemoveDefaultLoadingScreen()

local started = os.clock()
local skipped = false
skip.Activated:Connect(function()
	skipped = true
end)
task.delay(SKIP_AFTER_SECONDS, function()
	skip.Visible = true
end)

-- Wait until the first copy of the game has arrived from the server
if not game:IsLoaded() then
	game.Loaded:Wait()
end

-- Preload the images, meshes and sounds in Workspace in STEPS batches, moving the bar after each
local items = workspace:GetDescendants()
local perStep = math.max(1, math.ceil(#items / STEPS))
for first = 1, #items, perStep do
	if skipped then
		break
	end
	ContentProvider:PreloadAsync(table.move(items, first, math.min(first + perStep - 1, #items), 1, {}))
	local done = math.min(first + perStep - 1, #items) / #items
	barFill.Size = UDim2.fromScale(done, 1)
	status.Text = string.format("Loading %d%%", math.floor(done * 100))
end

local shownFor = os.clock() - started
if shownFor < MIN_SECONDS and not skipped then
	task.wait(MIN_SECONDS - shownFor)
end

-- Fade everything out, then remove the screen
local fade = TweenInfo.new(0.5)
for _, object in gui:GetDescendants() do
	if object:IsA("TextLabel") or object:IsA("TextButton") then
		TweenService:Create(object, fade, { TextTransparency = 1, BackgroundTransparency = 1 }):Play()
	elseif object:IsA("Frame") then
		TweenService:Create(object, fade, { BackgroundTransparency = 1 }):Play()
	end
end
task.wait(0.5)
gui:Destroy()

RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts

Where to put it

Explorer: one LocalScript, the screen is built in code
ReplicatedFirst
  LoadingScreen LocalScript new

Settings you can change

TITLEIn the script
The text in the middle of the screen. Put your game's name here. Default: "My Game".
MIN_SECONDSIn the script
The shortest time the screen stays up, so it never just flashes on a fast connection. Default: 2.
SKIP_AFTER_SECONDSIn the script
When loading takes longer than this, a Skip button appears. Default: 8.

In a Studio playtest everything is already on your computer, so the screen shows for about MIN_SECONDS and the bar fills almost at once. To see a slow load, raise MIN_SECONDS while you test.

Type-checked with luau-lsp strict against Roblox's API definitions on 21 October 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.

Prompt for RoCode

Add a custom loading screen to my game: a LocalScript in ReplicatedFirst that removes the default loading screen, shows a full-screen ScreenGui with my game name and a progress bar while Workspace assets preload in batches, offers a Skip button after 8 seconds, stays up at least 2 seconds and fades out when the game is loaded.

On this page
  1. Get the script
  2. How it works
  3. 1. The script
  4. 2. Your own design
  5. 3. Test
  6. Common errors
  7. With RoCode
  8. Questions
  9. Sources

How loading works in Roblox

When a player joins, Roblox shows its own loading screen while the server sends a copy of your game to their device. Your game's scripts mostly start once that copy has arrived, which is too late to show anything during the wait. ReplicatedFirst is the exception: Roblox sends its contents to the player before anything else, and a LocalScript inside it starts straight away.

What a loading screen script uses
PieceWhat it does
ReplicatedFirstIts LocalScripts reach the player first and run before the rest of the game has loaded.
ReplicatedFirst:RemoveDefaultLoadingScreen()Removes Roblox's default loading screen, so yours is the one players see.
game:IsLoaded() and game.LoadedTell you when the first copy of the game has arrived.
ContentProvider:PreloadAsync()Waits until the images, meshes and sounds of the objects you pass have downloaded, so the game does not appear with grey textures.
A ScreenGui with IgnoreGuiInsetThe screen itself, covering the whole window including the strip at the top.

A loading screen does not make the game load faster. It hides the loading and gives players something to look at, which matters most on phones and slow connections, where the default screen can sit for a while.

Step 1: Add the loading screen script

  1. In the Explorer, hover over ReplicatedFirst, click + and insert a LocalScript.
  2. Rename it LoadingScreen and paste the code below.
  3. Change TITLE to your game's name, then press Play.
LocalScript in ReplicatedFirst named LoadingScreen
-- LocalScript in ReplicatedFirst named LoadingScreen.
-- Replaces Roblox's default loading screen with your own: a title, a progress bar,
-- a Skip button for slow connections, and a fade out when the game is ready.
local ContentProvider = game:GetService("ContentProvider")
local Players = game:GetService("Players")
local ReplicatedFirst = game:GetService("ReplicatedFirst")
local TweenService = game:GetService("TweenService")

local TITLE = "My Game"
local MIN_SECONDS = 2 -- show the screen at least this long, so it never just flashes
local SKIP_AFTER_SECONDS = 8 -- offer a Skip button when loading takes longer than this
local STEPS = 20 -- how many times the bar moves while assets preload

local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui") :: PlayerGui

-- The screen is built in code, so it exists before anything else in the game has loaded
local gui = Instance.new("ScreenGui")
gui.Name = "LoadingScreen"
gui.IgnoreGuiInset = true -- also cover the strip at the top of the screen
gui.DisplayOrder = 100 -- in front of every other ScreenGui
gui.ResetOnSpawn = false

local background = Instance.new("Frame")
background.Size = UDim2.fromScale(1, 1)
background.BackgroundColor3 = Color3.fromRGB(20, 16, 32)
background.BorderSizePixel = 0
background.Parent = gui

local title = Instance.new("TextLabel")
title.Text = TITLE
title.Font = Enum.Font.GothamBold
title.TextSize = 42
title.TextColor3 = Color3.new(1, 1, 1)
title.BackgroundTransparency = 1
title.AnchorPoint = Vector2.new(0.5, 0.5)
title.Position = UDim2.fromScale(0.5, 0.42)
title.Size = UDim2.fromOffset(600, 60)
title.Parent = background

local barBack = Instance.new("Frame")
barBack.AnchorPoint = Vector2.new(0.5, 0.5)
barBack.Position = UDim2.fromScale(0.5, 0.54)
barBack.Size = UDim2.fromOffset(320, 10)
barBack.BackgroundColor3 = Color3.fromRGB(60, 52, 84)
barBack.BorderSizePixel = 0
barBack.Parent = background

local barFill = Instance.new("Frame")
barFill.Size = UDim2.fromScale(0, 1)
barFill.BackgroundColor3 = Color3.fromRGB(160, 120, 255)
barFill.BorderSizePixel = 0
barFill.Parent = barBack

local status = Instance.new("TextLabel")
status.Text = "Loading..."
status.Font = Enum.Font.Gotham
status.TextSize = 18
status.TextColor3 = Color3.fromRGB(200, 190, 230)
status.BackgroundTransparency = 1
status.AnchorPoint = Vector2.new(0.5, 0)
status.Position = UDim2.fromScale(0.5, 0.57)
status.Size = UDim2.fromOffset(320, 24)
status.Parent = background

local skip = Instance.new("TextButton")
skip.Text = "Skip"
skip.Font = Enum.Font.GothamBold
skip.TextSize = 18
skip.TextColor3 = Color3.new(1, 1, 1)
skip.BackgroundColor3 = Color3.fromRGB(60, 52, 84)
skip.AnchorPoint = Vector2.new(0.5, 0)
skip.Position = UDim2.fromScale(0.5, 0.64)
skip.Size = UDim2.fromOffset(120, 36)
skip.Visible = false
skip.Parent = background

gui.Parent = playerGui
ReplicatedFirst:RemoveDefaultLoadingScreen()

local started = os.clock()
local skipped = false
skip.Activated:Connect(function()
	skipped = true
end)
task.delay(SKIP_AFTER_SECONDS, function()
	skip.Visible = true
end)

-- Wait until the first copy of the game has arrived from the server
if not game:IsLoaded() then
	game.Loaded:Wait()
end

-- Preload the images, meshes and sounds in Workspace in STEPS batches, moving the bar after each
local items = workspace:GetDescendants()
local perStep = math.max(1, math.ceil(#items / STEPS))
for first = 1, #items, perStep do
	if skipped then
		break
	end
	ContentProvider:PreloadAsync(table.move(items, first, math.min(first + perStep - 1, #items), 1, {}))
	local done = math.min(first + perStep - 1, #items) / #items
	barFill.Size = UDim2.fromScale(done, 1)
	status.Text = string.format("Loading %d%%", math.floor(done * 100))
end

local shownFor = os.clock() - started
if shownFor < MIN_SECONDS and not skipped then
	task.wait(MIN_SECONDS - shownFor)
end

-- Fade everything out, then remove the screen
local fade = TweenInfo.new(0.5)
for _, object in gui:GetDescendants() do
	if object:IsA("TextLabel") or object:IsA("TextButton") then
		TweenService:Create(object, fade, { TextTransparency = 1, BackgroundTransparency = 1 }):Play()
	elseif object:IsA("Frame") then
		TweenService:Create(object, fade, { BackgroundTransparency = 1 }):Play()
	end
end
task.wait(0.5)
gui:Destroy()

PreloadAsync waits for every asset in the objects it is given. Passing the whole Workspace in one call would leave the bar stuck at 0% until the end, so the script splits it into STEPS batches and moves the bar after each one.

The screen goes into PlayerGui directly because StarterGui has not been copied to the player yet when this script runs. ResetOnSpawn is off so the screen survives the character spawning underneath it.

Step 2: Use a screen you designed in Studio

Building a screen in code is quick, but you may want to design it in Studio with images and your own layout. Design it as a ScreenGui in StarterGui, name it LoadingScreen, then drag it onto the LoadingScreen LocalScript in ReplicatedFirst, so it becomes the script's child and arrives with it. Use this shorter script instead of Step 1's:

LocalScript in ReplicatedFirst named LoadingScreen
-- LocalScript in ReplicatedFirst named LoadingScreen, with your own ScreenGui named
-- LoadingScreen inside it (design it in StarterGui, then drag it onto this script).
local Players = game:GetService("Players")
local ReplicatedFirst = game:GetService("ReplicatedFirst")

local MIN_SECONDS = 2

local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui") :: PlayerGui
local gui = (script:WaitForChild("LoadingScreen") :: ScreenGui):Clone()
gui.IgnoreGuiInset = true
gui.DisplayOrder = 100
gui.ResetOnSpawn = false
gui.Parent = playerGui
ReplicatedFirst:RemoveDefaultLoadingScreen()

local started = os.clock()
if not game:IsLoaded() then
	game.Loaded:Wait()
end
local shownFor = os.clock() - started
if shownFor < MIN_SECONDS then
	task.wait(MIN_SECONDS - shownFor)
end
gui:Destroy()

Step 3: Test it

  1. Open the Output window (Window menu, then Output) and press Play. Your screen should appear, the bar fill, and the screen fade after about MIN_SECONDS.
  2. Studio already has every asset on your computer, so the bar fills almost at once. Set MIN_SECONDS to 8 and play again to check the Skip button appears and works.
  3. Publish and join from the Roblox app on a phone to see the screen as players will.

Common errors and fixes

If the default loading screen still shows, or yours never goes away, check these first.

Roblox's default loading screen still shows

The script is not a LocalScript in ReplicatedFirst, or it errors before it reaches RemoveDefaultLoadingScreen(). Check the Output window for a red line naming LoadingScreen.

My screen appears late, after the game is visible

The script is in StarterGui or StarterPlayerScripts. Move it to ReplicatedFirst.

Infinite yield possible on 'ReplicatedFirst.LoadingScreen:WaitForChild("LoadingScreen")'

Step 2's script cannot find your ScreenGui. It must be a child of the script and named exactly LoadingScreen.

The screen never goes away

Something after game.Loaded errored or never finished. Check Output; the Skip button in Step 1 is also there so a player is never stuck.

A gap shows at the top of the screen

Set IgnoreGuiInset to true on the ScreenGui, as both scripts do.

Build it with RoCode

RoCode is an AI agent that works inside Roblox Studio through a plugin. You describe the screen you want, and it builds it in the place you have open.

You type

Add a loading screen with my game's name, a purple progress bar and a Skip button, that fades into the game when it is loaded.

RoCode does
  • Writes the LocalScript in ReplicatedFirst and builds the ScreenGui
  • Matches the colours and font you ask for, and can reuse a ScreenGui you already designed
  • Checks the scripts it wrote inside Studio before it finishes

You still press Play and check the screen looks right on a phone and a computer. How RoCode connects to Studio.

Questions

Where do I put a loading screen script in Roblox?

In ReplicatedFirst, as a LocalScript. It is the only place a script runs before the rest of the game has loaded.

How do I remove the default Roblox loading screen?

Call game:GetService("ReplicatedFirst"):RemoveDefaultLoadingScreen() from a LocalScript in ReplicatedFirst, once your own screen is showing.

Does a loading screen make my game load faster?

No. It covers the wait. To shorten it, use fewer and smaller images, meshes and sounds near the spawn.

Why does my loading screen only show for a moment in Studio?

In a Studio playtest everything is already on your computer. Raise MIN_SECONDS while testing, or test the published game on a phone.

Sources and further reading

How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 21 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.