Building a FiveM loading screen with React and Vite

12 min read6 sections · 4 questions answered
Short answer

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

Inside your resource folder
npm create vite@latest web -- --template react-ts
cd web
npm install

Step 2: vite.config

web/vite.config.ts
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

fxmanifest.lua
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

web/src/useLoading.ts
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 };
}
web/src/App.tsx
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:

web/src/main.tsx (excerpt)
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 dist size 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.

Common questions

Can I use React for a FiveM loading screen?

Yes. Build it with Vite (base ./), point loadscreen at the built index.html and list the built files under files.

Why is my React loading screen blank in FiveM?

Almost always absolute asset paths. Set base: './' in vite.config and rebuild.

Do I upload node_modules to my server?

No. Only the built dist folder and the manifest are needed at runtime.

Does Vue or Svelte work too?

Yes. Any framework that builds to static files works the same way.

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

More guides