Building a FiveM loading screen with React and Vite
Create a Vite + React project, set base: './' in vite.config so asset paths are relative, and build into a folder inside the resource. Point the manifest at the built file with loadscreen 'web/dist/index.html' and list web/dist/** under files. Listen to loading messages in a useEffect hook, and mock them during npm run dev.
For a loading screen with tabs, staff cards, a rules browser and animated widgets, plain HTML gets unwieldy fast. A component framework like React with the Vite build tool keeps it organised — and produces a static bundle FiveM loads exactly like a hand-written page. The only FiveM-specific parts are two build settings and one event hook.

#Step 1: create the project
npm create vite@latest web -- --template react-ts
cd web
npm install#Step 2: vite.config
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
base: './', // relative asset paths — essential for FiveM
build: {
outDir: 'dist',
assetsInlineLimit: 0, // keep fonts and images as files
},
});#Step 3: the manifest
fx_version 'cerulean'
game 'gta5'
loadscreen 'web/dist/index.html'
loadscreen_cursor 'yes'
files {
'web/dist/**',
}When you distribute or deploy the resource, include fxmanifest.lua and web/dist — not web/src or node_modules, which are only needed to build.
#Step 4: a hook for loading events
import { useEffect, useState } from 'react';
export function useLoading() {
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState('Connecting…');
useEffect(() => {
const onMessage = (e: MessageEvent) => {
const d = e.data || {};
if (d.eventName === 'loadProgress') {
setProgress((p) => Math.max(p, Math.min(1, d.loadFraction)));
}
if (d.eventName === 'onLogLine') setStatus(d.message);
};
window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
}, []);
return { progress, status };
}import { useLoading } from './useLoading';
export default function App() {
const { progress, status } = useLoading();
const name = (window as any).nuiHandoverData?.name;
return (
<main className="safe">
<img className="logo" src="./logo.png" alt="My City" />
<p>{name ? `Welcome, ${name}` : 'Welcome'}</p>
<div className="bar"><div className="fill" style={{ width: `${progress * 100}%` }} /></div>
<p className="status">{status}</p>
</main>
);
}#Developing without the game
Run npm run dev and work in the browser with hot reload. Add a small development-only mock so the progress bar moves:
if (import.meta.env.DEV) {
let f = 0;
const t = setInterval(() => {
f += 0.03;
window.postMessage({ eventName: 'loadProgress', loadFraction: Math.min(f, 1) }, '*');
if (f >= 1) clearInterval(t);
}, 200);
}#Keeping the bundle small
- Avoid heavy UI libraries for a single screen; a few components and plain CSS are enough.
- Import only the icons you use.
- Compress images to WebP and host big video on a CDN — see loading screen performance.
- Check
distsize after each build; a few hundred KB of JavaScript is plenty.
The same setup powers in-game NUI interfaces, with an extra layer for talking to Lua — see React NUI project structure.
Questions
Can I use React for a FiveM loading screen?
./), point loadscreen at the built index.html and list the built files under files.Why is my React loading screen blank in FiveM?
base: './' in vite.config and rebuild.Do I upload node_modules to my server?
dist folder and the manifest are needed at runtime.Does Vue or Svelte work too?
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.