A lot of custom loading screens just fake the progress bar with a CSS animation that loops no matter what's actually downloading. You don't have to. The Source engine calls JavaScript functions on your page with the REAL download state, and this guide covers how to wire them into a progress bar that tells the truth.
The five callbacks
Define these as global functions (on window) and the engine invokes them during loading:
GameDetails(servername, serverurl, mapname, maxplayers, steamid, gamemode, ...)— server metadata, called once. Good for showing the server name and map without hardcoding them.SetFilesTotal(total)— the total file count for this join. Your denominator.SetFilesNeeded(needed)— remaining files, called repeatedly as downloads complete. Your numerator (inverted).DownloadingFile(fileName)— the file currently downloading. Handy for a status line.SetStatusChanged(status)— engine phase text: retrieving server info, sending client info, workshop status…
A complete working progress bar
<div id="status">Connecting…</div>
<div class="bar"><div id="fill"></div></div>
<div id="file"></div>
<script>
var total = 0;
window.GameDetails = function (servername, serverurl, mapname) {
document.title = servername;
document.getElementById("status").textContent =
"Joining " + servername + " — " + mapname;
};
window.SetFilesTotal = function (t) { total = t; };
window.SetFilesNeeded = function (needed) {
if (total > 0) {
var done = total - needed;
var percent = Math.round((done / total) * 100);
document.getElementById("fill").style.width = percent + "%";
}
if (needed === 0) {
document.getElementById("status").textContent = "Almost there…";
}
};
window.DownloadingFile = function (name) {
document.getElementById("file").textContent = name;
};
window.SetStatusChanged = function (status) {
document.getElementById("status").textContent = status;
};
</script>Edge cases the simple version misses
- Zero files to download — returning players often need nothing at all. SetFilesTotal may report 0, or never fire in any meaningful way. Show an indeterminate state until you know, not a stuck 0%.
- Workshop downloads — large workshop content comes through as status changes rather than per-file callbacks, so surface SetStatusChanged or the bar will look frozen. One server sat on a workshop pack for two minutes with a dead-still bar because the page ignored the status text.
- Very fast loads — on a warm cache the whole sequence is over in under a second. Don't animate the bar so slowly that it never visually reaches the end.
- JS errors — one uncaught exception in a callback can take down everything after it. Keep the callbacks defensive.
How Lumescreen handles it
Every Lumescreen theme ships with these callbacks already wrapped and normalized. The wrapper catches the engine events, hands them to the theme's UI, and at the same time measures the real load metrics: how long loading takes, how many players make it through, where things fail. That's the data feeding the retention analytics. You get a progress bar players trust and numbers you can actually act on.
Building your own? The full cvar-side reference lives in the sv_loadingurl complete guide. Rather have it done for you? Pick a theme and the progress bar is already wired up.