Build your own FiveM loading screen from scratch
Create a resource folder with fxmanifest.lua (declaring loadscreen 'index.html', loadscreen_cursor 'yes' and all files under files), an index.html with a background, logo, progress bar and audio element, a style.css using viewport units and object-fit: cover, and a script.js that listens for loadProgress and onLogLine messages. Add ensure for it, disable any other loading screen and restart.
You can build a good loading screen with nothing but a text editor: one manifest, one HTML file, one stylesheet and one script. This tutorial builds a complete one — responsive background, logo, a real progress bar, a status line, music with a mute button and a personal greeting — and explains each part so you can change it afterwards.

#The folder
my_loadscreen/
├── fxmanifest.lua
├── index.html
├── style.css
├── script.js
├── img/
│ ├── bg.webp
│ └── logo.png
└── music/
└── theme.mp3#Step 1: fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
name 'my_loadscreen'
description 'Custom loading screen'
loadscreen 'index.html'
loadscreen_cursor 'yes'
files {
'index.html',
'style.css',
'script.js',
'img/*',
'music/*',
}Each directive is explained in the loading screen manifest guide.
#Step 2: index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
</head>
<body>
<img class="bg" src="img/bg.webp" alt="">
<div class="shade"></div>
<main class="safe">
<img class="logo" src="img/logo.png" alt="My City">
<p class="welcome" id="welcome">Welcome to My City</p>
<div class="bar"><div class="fill" id="fill"></div></div>
<p class="status" id="status">Connecting…</p>
</main>
<button class="mute" id="mute" aria-label="Mute music">🔊</button>
<audio id="music" src="music/theme.mp3" autoplay loop></audio>
<script src="script.js"></script>
</body>
</html>#Step 3: style.css
html, body { margin: 0; height: 100%; overflow: hidden; background: #05070d; font-family: system-ui, sans-serif; color: #fff; }
.bg { position: fixed; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.shade { position: fixed; inset: 0; background: linear-gradient(180deg, rgba(5,7,13,.15), rgba(5,7,13,.8)); }
.safe {
position: fixed; inset: 0; margin: auto;
width: min(100vw, calc(100vh * 16 / 9)); aspect-ratio: 16 / 9;
display: flex; flex-direction: column; align-items: center; justify-content: flex-end;
gap: 1.6vh; padding-bottom: 9vh; box-sizing: border-box;
}
.logo { width: clamp(180px, 20vw, 560px); height: auto; margin-bottom: auto; margin-top: 18vh; }
.welcome { font-size: clamp(16px, 1.3vw, 28px); opacity: .9; margin: 0; }
.bar { width: min(560px, 60vw); height: 6px; border-radius: 99px; background: rgba(255,255,255,.15); overflow: hidden; }
.fill { height: 100%; width: 0; background: #5b6af0; transition: width .4s ease; }
.status { font-size: clamp(12px, .85vw, 18px); opacity: .7; margin: 0; }
.mute { position: fixed; right: 2vw; bottom: 2vw; width: 44px; height: 44px; border-radius: 12px; border: 1px solid rgba(255,255,255,.2); background: rgba(0,0,0,.4); color: #fff; font-size: 18px; cursor: pointer; }Viewport units and a 16:9 safe area keep this correct on every resolution — the reasoning is in responsive loading screens.
#Step 4: script.js
const fill = document.getElementById('fill');
const status = document.getElementById('status');
const music = document.getElementById('music');
const mute = document.getElementById('mute');
let shown = 0;
// Greeting from the server, if it sends one (deferrals.handover)
const hand = window.nuiHandoverData || {};
if (hand.name) document.getElementById('welcome').textContent = `Welcome, ${hand.name}`;
// Loading events from FiveM
window.addEventListener('message', (e) => {
const d = e.data || {};
if (d.eventName === 'loadProgress') {
shown = Math.max(shown, Math.min(1, d.loadFraction));
fill.style.width = (shown * 100).toFixed(1) + '%';
}
if (d.eventName === 'onLogLine') status.textContent = d.message;
});
// Music: gentle volume, remembered mute
music.volume = 0.3;
music.muted = localStorage.getItem('muted') === '1';
mute.textContent = music.muted ? '🔇' : '🔊';
mute.addEventListener('click', () => {
music.muted = !music.muted;
localStorage.setItem('muted', music.muted ? '1' : '0');
mute.textContent = music.muted ? '🔇' : '🔊';
});#Step 5: install and test
- Open
index.htmlin Chrome or Edge and check the layout at a few window sizes. - Put the folder in
resources/and addensure my_loadscreento server.cfg. - Disable any other loading screen resource (
qb-loadingscreen,esx_loadingscreenor similar) — only one may declareloadscreen. - Restart the server and connect.
#Where to take it next
- A video background instead of an image — video backgrounds.
- Rules, staff and a tips carousel — rules and info.
- Keep it up until your spawn is ready — manual shutdown.
- A component-based build with React — React and Vite loading screens.
Questions
How do I make my own FiveM loading screen?
loadscreen 'index.html' and lists every file under files, write the HTML, CSS and JavaScript, then ensure it and disable any other loading screen.Do I need to know JavaScript?
Why does my loading screen show but images are missing?
files in the manifest, so the client never downloads them.Can I test the loading screen without FiveM?
index.html in a desktop browser. Only the loading events need the game; you can fake them from the console.Ready to pick a map?
Twelve themes on three base map styles, $8 each, instant download.
Relevant to what you just read
The tool for this
A drag-and-drop loading screen, hosted for you From Free, built in the browser, exported as a drop-in resource.
Look it up while you work
The reference tables this guide leans on, free and without an account.
- The map modding glossary38 terms — file formats, radar parts, framework words — each with a one-line answer first.