Grand opening25% off everything with FAM-261023-261025-UL4B9this weekendAdded for you at checkout Halloween Edition36 maps, limited runlimited runShop the drop

4 September 2026 · 10 MIN READ

The gap between a loading screen that works and one somebody made

The details that separate a loading screen somebody built from a template with a name swapped in, from pacing to motion cost to the specific tells.

A loading screen is the easiest thing on a FiveM server to get technically working and the hardest to get right. An HTML file, one line in a manifest, and it renders. The problem is that the bar for working is so low that almost every server clears it, which means the loading screen is not a differentiator because it exists. It is a differentiator because of about eight decisions, none of which are visible as decisions once they are made correctly.

Pace it against a real join, not against your test

When you test your own loading screen you see it for four seconds, because your cache is warm and your resources are local. A player joining for the first time on a server with three hundred resources sees it for well over a minute, sometimes two. Every design decision should be made against the long case, because the short case is forgiving of anything.

That length changes what belongs on screen. A composition that is pleasant for five seconds can be actively irritating at second forty, and the things that cross over are specific: anything that loops faster than about four seconds, anything that flashes, any animation with a hard reset rather than a seamless return, and any text that cycles. A three-second tip rotation is charming twice and maddening on the fifteenth pass.

What survives a minute is slow continuous motion with no discernible loop point. Drifting cloud layers, a parallax that never repeats within the window, embers rising, a glint that crosses the frame over twelve seconds. The player should be unable to tell you how long the loop is, which means there is effectively nothing to get bored of.

The status line beats a progress bar

Progress bars on FiveM loading screens are usually fiction. The honest reason is that the client does report progress, but not in a way that maps to a smooth bar: the fraction arrives in uneven jumps and then stops moving entirely during the longest phase, which is resource download and the initial map load. A bar that sits at 73 per cent for forty seconds is worse than no bar, because it tells the player something is wrong.

The common response is to fake it, animating a bar to full over a guessed duration. That is dishonest in a way players notice, because the bar reaches 100 per cent and nothing happens. The better answer is a status line: one line of small type saying what the client is currently doing, driven by the real phase events. It is always truthful, it never stalls visibly because the message changes even when a fraction does not, and it costs nothing to implement.

The packs ship with the progress bar off by default for exactly that reason, and with the real phase messages wired up. If you want a bar, use it as a secondary element, thin and quiet, underneath a status line that is doing the actual communicating.

What the phase events let you say

The client posts messages into the loading screen page as it works through startup. You listen on the window and read eventName. Two of them carry useful data and the rest are phase markers, which is to say they tell you where in the sequence the client is and nothing more. That is enough for a status line.

The phase events the client posts, and what each one is worth saying
EventWhat the client is doingA line that is honest
startInitFunctionBeginning an initialisation stageINITIALISING
startDataFileEntriesQueueing data files, with a countREADING DATA FILES
performMapLoadFunctionStreaming and building the mapBUILDING THE MAP
startInitFunctionOrderWorking through the init orderSTARTING UP
initFunctionInvokingRunning resource init functionsLOADING RESOURCES
endInitFunctionFinishing an initialisation stageALMOST THERE
loadProgressReporting loadFraction, 0 to 1Feed a bar, not a line
var MSG = {
  startInitFunction: "INITIALISING",
  startDataFileEntries: "READING DATA FILES",
  performMapLoadFunction: "BUILDING THE MAP",
  startInitFunctionOrder: "STARTING UP",
  initFunctionInvoking: "LOADING RESOURCES",
  onLogLine: null,
  endInitFunction: "ALMOST THERE"
};

window.addEventListener("message", function (e) {
  var d = e.data || {};
  if (d.eventName === "loadProgress" && typeof d.loadFraction === "number") {
    window.__progress = d.loadFraction;
    return;
  }
  if (d.eventName && Object.prototype.hasOwnProperty.call(MSG, d.eventName)) {
    var m = MSG[d.eventName];
    if (m) window.__loadMsg = m;
  }
});

onLogLine is mapped to null deliberately. It fires constantly with raw client log lines, and putting those on screen is a specific kind of mistake: it looks like debugging output because it is debugging output. Consume the event so it does not fall through to a default handler, and show nothing.

A note on wording. Keep the lines short, keep them in one voice, and do not write them in the first person on behalf of the server. 'BUILDING THE MAP' is fine. 'We are building the map for you!' is not, because it is six words to say one thing and because the exclamation mark will be on screen for forty seconds.

Badges, tips and the noise floor

Three short badges work. Something like FIVEM RP, 18+ WHITELIST, EU SERVER: a row of small capitalised labels that tell a player what kind of place they have joined and reinforce it on every join. They are static, they cost nothing, and they are the only element on a loading screen doing an informational job rather than a decorative one.

Tip lines are a different matter and they default to off for a reason. A single tip, static, genuinely useful, is fine: a keybind they will need in the first minute, for instance. What does not work is a rotating list of twenty tips, because the player reads one, starts the second, and the text changes under them. If you must rotate, rotate once per join rather than on a timer, so each player sees one tip and it is a different one next time.

Where badges become noise is when they stop being information. Four badges is one too many. Badges carrying marketing claims rather than facts are noise. A badge saying CUSTOM SCRIPTS is noise, because every server says it and no player has ever chosen a server on the basis of it. The test is whether a new player could act differently as a result of reading it.

Why the music is off by default

A loading screen with music is a real improvement and shipping it on by default is still wrong, for two reasons. The first is technical: browser engines block autoplay with sound until the user has interacted with the page, and the loading screen is a browser engine. An audio element whose play call is rejected leaves you with silence and a promise rejection in the console, which looks like a broken file rather than a policy decision.

The second is that the player may already have something playing. A sizeable share of people joining your server are listening to music, in a Discord call, or watching the streamer whose server this is, and overriding that at full volume is the single most intrusive thing a loading screen can do. Setting it off, leaving it as two lines to enable, and keeping the volume at roughly a third is the right default.

If you do enable it, attempt playback immediately and fall back to starting on the first click. That works because the manifest declares the cursor visible, so there is something to click with. Remove the listener once it fires, or you get a second overlapping playback the next time anybody clicks.

The manifest, and what the cursor line implies

fx_version 'cerulean'
game 'gta5'

name 'velora_loading'
description 'Velora loading screen'
version '1.0.0'

loadscreen 'index.html'
loadscreen_cursor 'yes'

files {
  'index.html',
  'style.css',
  'scene.js',
  'app.js',
  'fonts/*.ttf'
}

Every file the page references has to appear in files, including the fonts, and the glob is the only sane way to handle a font directory. A missing entry does not error anywhere you will see it; the page simply renders without that asset, which is how people end up with a loading screen in a fallback typeface and no idea why.

loadscreen_cursor 'yes' deserves a thought rather than a copy and paste. With it on, the player has a visible pointer, which lets you put a clickable element on screen and lets the music fallback work. With it off, the screen is cleaner. The reason to keep it on is the music; the reason it sometimes looks wrong is a visible cursor hovering over a page with nothing clickable on it. If you have no interactive element and no audio, turn it off.

Typography across 1080p, 1440p and ultrawide

Author the composition at 1920x1080 and scale the whole thing with one factor. Not media queries, not separate layouts, one number read from the window and applied to the root, with every size in the stylesheet expressed as a multiple of it.

function fit() {
  var s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
  document.documentElement.style.setProperty("--s", String(s));
  stage.style.width = window.innerWidth + "px";
  stage.style.height = window.innerHeight + "px";
}
fit();
window.addEventListener("resize", fit);

The min of the two ratios is what makes this work everywhere. On 1440p both ratios are above one and the factor is 1.33, so the lockup grows proportionally and the type stays at the same visual size relative to the frame. On a 3440x1440 ultrawide the width ratio is 1.79 and the height ratio is 1.33, so the factor is 1.33 and the extra width becomes background rather than stretching the composition. The stage is sized to the real window separately, so the backdrop still fills the screen.

The failure mode to avoid is using viewport units directly for type. Font sizes in vw look correct at 1080p and then become absurd on an ultrawide, because the width grew by 79 per cent while the height did not grow at all. Anything that should scale with the composition scales with the factor; anything that should fill the screen uses the viewport. Mixing the two is where ultrawide loading screens go wrong.

Motion restraint, because the client is busy

A loading screen runs at the worst possible moment. The client is streaming assets, decompressing, building collision and saturating a disk, and your page is competing for the same machine. A loading screen that drops to eight frames a second is worse than a still image, and it is entirely a function of what you animate rather than how much.

Animate transform and opacity. Nothing else. Those two are composited on the GPU without touching layout or repainting the element, which is why a dozen layers drifting on transforms costs almost nothing while a single element animating its width costs a layout pass every frame.

  • width, height, top, left, margin and padding force a layout recalculation every frame. This is the one people get wrong, because animating a progress bar's width is the obvious implementation. Scale it on the X axis from a left origin instead.
  • box-shadow, filter and backdrop-filter force a repaint, and blur radii are expensive in proportion to the radius. A pulsing glow built from a box-shadow animation is the most costly thing on most loading screens.
  • background-position animates cheaply enough on small layers and badly on full-screen ones. For a drifting backdrop, translate an oversized element instead.
  • Promote the layers that move with a transform on the compositor and then leave them alone. Adding will-change to thirty elements is its own problem, since each one becomes a separate composited layer with its own memory.

The practical version: build the scene from a handful of layers that each translate or fade, keep blurs baked into the artwork rather than applied live, and if something must appear to grow, scale it. A scene built that way holds its frame rate on a machine that is otherwise fully committed, which is the only condition it will ever run under.

The tells of a free template

Anybody who server-hops recognises the circulating free templates within two seconds, and the recognition is not about quality. It is about a small set of details that nobody bothers to change.

  1. A video background at 1920x1080 upscaled on a 1440p monitor, usually a city flyover with visible compression in the sky gradient.
  2. The server name in a different typeface from everything else, because it was typed into a field rather than set as part of the composition.
  3. Social icons for platforms the server does not use. A Twitter bird and a YouTube button linking to the template author's placeholder.
  4. A progress bar that animates smoothly to full in twelve seconds regardless of the actual load, then sits there.
  5. A song that another server also uses, which is a surprisingly strong tell because people remember it.
  6. A member count or player count that is hardcoded, usually at a round number, since the loading screen has no connection to the server state.
  7. Spotify or music player furniture with no audio wired to it.

Half of those are fixable in an afternoon on a template you already have, which is worth saying because the choice is not only between a free template and a commission. Delete the dead social icons, remove the fake counter, cut the progress bar or wire it to the real fraction, and set the name in the right typeface. What is left is a loading screen that looks like somebody looked at it, which is the entire claim being made here.

Read next

Closest to this one by subject.