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.mp3Step 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
- 1Open
index.htmlin Chrome or Edge and check the layout at a few window sizes. - 2Put the folder in
resources/and addensure my_loadscreento server.cfg. - 3Disable any other loading screen resource (
qb-loadingscreen,esx_loadingscreenor similar) — only one may declareloadscreen. - 4Restart 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.
Common questions
How do I make my own FiveM loading screen?
Create a resource with a manifest that declares 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?
Only a little: the script in this tutorial is enough for a progress bar, status line and music. Everything else is HTML and CSS.
Why does my loading screen show but images are missing?
The image files are not listed under files in the manifest, so the client never downloads them.
Can I test the loading screen without FiveM?
Yes, open index.html in a desktop browser. Only the loading events need the game; you can fake them from the console.
Want a loading screen you never have to debug?
Build it in the browser, export once, and edit it whenever — no HTML, no re-uploads.
Start free