# Building a FiveM loading screen with React and Vite

> Use React (or any Vite framework) for a FiveM loading screen: Vite config with a relative base, building into the resource, the manifest with loadscreen and glob files, listening to loading events in a hook, mocking events in development and keeping the bundle small.

Source: https://fam-tools.com/guides/fivem-loading-screen-react-vite · FAM Tools · published 2026-09-11 · updated 2026-09-11 · about 12 min

**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`.

## Key points

- `base: './'` is required — absolute `/assets/…` paths break inside FiveM.
- Ship only the built `dist` folder; never the `node_modules`.
- List built files with a glob: `web/dist/**`.
- Loading events arrive as window `message` events — wrap them in a hook.
- Keep the bundle small; a loading screen is not a web app.

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*

```bash
npm create vite@latest web -- --template react-ts
cd web
npm install
```

## Step 2: vite.config

*web/vite.config.ts*

```typescript
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
  },
});
```

> **The blank screen bug** — Without `base: './'`, Vite writes absolute paths like `/assets/index.js`. FiveM serves the page from the resource, so those paths point nowhere and the screen stays blank. It is the most common React loading-screen problem.

## Step 3: the manifest

*fxmanifest.lua*

```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*

```tsx
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*

```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)*

```tsx
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](https://fam-tools.com/guides/fivem-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](https://fam-tools.com/guides/fivem-nui-react-boilerplate-structure).

## 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.

## Related

- https://fam-tools.com/guides/fivem-loading-screen-html-tutorial
- https://fam-tools.com/guides/fivem-nui-react-boilerplate-structure
- https://fam-tools.com/guides/fivem-loading-screen-performance
- https://fam-tools.com/guides/fivem-loading-screen-progress-bar
