WebUI & HUD
Custom loading screens
Detect native loading cycles, display a branded WebUI with progress, and hide or restore HUDs during teleport loads.
On this page4 sections
Client Lua can observe Cyberpunk's native loading-screen lifecycle, including the screen sometimes opened by a long-distance teleport. Use the state to show your own WebUI or temporarily hide your resource's HUD. The API is read-only: it does not force a loading screen, skip it, finish streaming or teleport anyone.
This API requires an updated Open77 client. Feature-detect it on older clients; no network protocol or dedicated-server update is required.
| What you want to change | Use |
|---|---|
| Artwork, logo, tips or progress shown during a native load | A resource WebUI driven by the events on this page. |
| Your gamemode HUD during a teleport load | Hide its WebUI while active is true; restore its normal visibility policy afterwards. |
| A scripted fade before a teleport or cinematic | Native screen transitions. |
| Open77's initial connection or resource download screen | The launcher and connection flow; this API does not replace it. |
Customization adds your own cover over the native screen. It does not replace Cyberpunk's loading assets or change how long the engine needs to load.
-- open77.lua
client_script 'client.lua'
permissions { 'screen.read' }
| Client function | Result |
|---|---|
Open77.screen.isLoading() |
true/false, or nil, reason if unavailable. |
Open77.screen.loadingState() |
State table, or nil, reason if unavailable. |
Both observe the same process-wide state, regardless of which resource caused the transition. A resource reload does not reset that state.
| State field | Meaning |
|---|---|
active |
The native loading lifecycle is active, including opening and delayed closing. |
id |
Opaque string identifying this loading cycle within this client process. "0" before its first observed cycle. |
revision |
Increasing change number; useful for discarding older snapshots. |
kind |
Native screen type: "unknown", "splash", "initial" or "fastTravel". It can become known after the start event. |
progressKnown |
Whether the native progress bar has reported a value during this cycle. |
progress |
Number from 0 to 1 when known; otherwise nil. This is the engine's value and may move backwards between native stages. |
elapsedMs |
Real elapsed milliseconds since the cycle began. Frozen after it ends. |
At idle, the table retains the most recent cycle's ID, kind and progress. A new
cycle clears its progress. 100% is not a finished event: wait for active
to become false. No estimated timer is used to close the screen.
kind describes a native screen, not the cause. A teleport may use
"unknown"; "fastTravel" is also used during Open77's initial world load.
active is not a pixel-level visibility test: a brief loading transition may
complete before a full-screen illustration is drawn. Background streaming,
quest fades, your WebUI and Open77's connection/download screen are separate.
For quest fades use isFaded / nativeState.
Events
These local client events each receive one state table and require
screen.read on the receiving resource:
| Event | Payload | When |
|---|---|---|
open77:loadingScreen:started |
(state) |
Once when a new native loading cycle starts. |
open77:loadingScreen:changed |
(state) |
After an observed active/kind/progress change. Intermediate progress updates are coalesced. |
open77:loadingScreen:finished |
(state) |
Once when the native delayed-hide callback completes the cycle. |
Start and finish are each followed by changed. A short cycle can produce both
edges in one Lua tick. Payloads describe the event's instant; a state query can
already be newer. Do not assume callbacks execute during a blocked engine
frame: they are delivered on the next client scripting tick. These events are
not network events and cannot be generated through TriggerEvent or a server
message. The legacy shell-only open77:loading:progress remains unchanged.
Hide your own WebUI while loading
Integrate this in the resource that owns hudPage. Recompute visibility from
your resource's normal policy so a closed menu is not accidentally reopened.
if Open77.screen and Open77.screen.loadingState then
local loading, revision = false, -1
local function renderHudVisibility()
if hudEnabled and not loading then hudPage:show()
else hudPage:hide() end
end
local function apply(state)
if not state or state.revision < revision then return end
revision = state.revision
loading = state.active
-- Also call renderHudVisibility when hudEnabled changes.
renderHudVisibility()
end
AddEventHandler('open77:loadingScreen:changed', apply)
-- Essential: the resource can start after the started event.
local state, err = Open77.screen.loadingState()
if state then apply(state) else print(err) end
end
Do not make gameplay authority depend on a client's report of loading state. It is presentation information, not proof that a player or destination is ready.
Cover the native screen and hide the vanilla HUD
Create resources/my_loading_screen/ with these three files. The HTML is kept
ready and transparent between loads, so Chromium does not need to start when
the engine begins loading. The page takes no keyboard or mouse focus.
1. Declare the resource
-- open77.lua
resource 'my_loading_screen'
version '1.0.0'
auto_start true
reload_policy 'reconnect'
client_script 'client.lua'
web_ui_page 'web/index.html'
web_ui_auto_create false
files { 'web/index.html' }
permissions { 'screen.read', 'webui.modal', 'ui.vanilla.hud' }
2. Forward the loading state to your page
-- client.lua
if not Open77.screen or not Open77.screen.loadingState then
print('[my_loading_screen] Native loading observation is unavailable.')
return
end
AddEventHandler('onClientResourceStart', function(name)
if name ~= GetCurrentResourceName() then return end
local page, reason = Open77.webui.create({
entry = 'web/index.html', layer = 'modal', zIndex = 100,
visible = true, transparent = true, fps = 30,
})
if not page then print(reason); return end
local ready, active, revision = false, false, -1
local function apply(state)
if not state or state.revision < revision then return end
revision = state.revision
if state.active ~= active then
active = state.active
-- Releases only this resource's HUD claims when loading ends.
Open77.hud.setVisible('all', not active)
end
if ready then page:send('loading:state', state) end
end
local function refresh()
local state, err = Open77.screen.loadingState()
if state then apply(state) else print(err) end
end
AddEventHandler('open77:loadingScreen:changed', apply)
page:on('ui:ready', function()
ready = true
refresh()
end)
-- Also handles a resource activated after the loading cycle started.
refresh()
end)
Create the page from onClientResourceStart, when presentation effects are
permitted. Register the listener before the initial state query. Wait for
ui:ready before sending to JavaScript; this avoids losing the first update.
3. Design the cover
<!-- web/index.html -->
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>My server loading screen</title>
<style>
:root { --accent: #00d7e6; --background: #071017; }
html, body { margin: 0; background: transparent; color: #e9f1f5;
font-family: system-ui, sans-serif; }
#cover { position: fixed; inset: 0; display: none; place-content: center;
text-align: center; background: var(--background); }
#cover.active { display: grid; }
.brand { color: var(--accent); letter-spacing: .2em; }
progress { width: min(420px, 75vw); height: 8px; accent-color: var(--accent); }
.tip { color: #b0c1cc; }
</style>
<section id="cover" aria-live="polite">
<p class="brand">MY SERVER</p>
<h1>Loading your destination</h1>
<progress id="bar" max="1" aria-label="Loading progress"></progress>
<p id="status">Loading…</p>
<p class="tip">Explore Night City with your crew.</p>
</section>
<script>
Open77.on('loading:state', state => {
document.querySelector('#cover').classList.toggle('active', state.active);
const bar = document.querySelector('#bar');
if (state.progressKnown) bar.value = state.progress;
else bar.removeAttribute('value'); // Native progress is not available yet.
document.querySelector('#status').textContent = state.progressKnown
? `${Math.round(state.progress * 100)}%` : 'Loading…';
});
Open77.ready();
</script>
</html>
Change MY SERVER, the heading, the tip and the CSS variables to match your
server. To add artwork, ship web/background.webp and web/logo.png, include
both in the manifest's files list, then use url('./background.webp') in the
cover's background and <img src="./logo.png" alt="My server"> in its markup.
Asset paths are relative to web/index.html.
Use a CSS spinner or an indeterminate <progress> while progressKnown is
false. Keep the cover visible at 100% until active becomes false. Never use
a fixed timeout or the progress percentage to decide that the load has ended.
4. Enable it on the server
Add my_loading_screen to the server configuration's resources.load array,
then reconnect a client to receive the resource and its files. Keep any other
configured resources in that array. Use a long-distance teleport that actually
triggers Cyberpunk's loading screen to check the cover and HUD restoration;
short teleports may not open one.
The repository also includes the optional
resources/examples/open77_loading_screen resource. Enable it using
examples/open77_loading_screen in resources.load; its resource name is
open77_loading_screen. Run only one loading cover at a time.
Stopping or reloading the resource destroys its page and releases its HUD claims automatically. It does not alter other resources' WebUIs; those should use the loading events in their own visibility policy.
Use a separate resource for temporary vanilla HUD claims if your existing
resource already keeps components hidden: releasing all releases every HUD
claim owned by that resource. See HUD ownership.
Server WebUIs remain subject to normal session presentation gates: this does
not allow a downloaded resource to replace Open77's initial connection screen.
Avoid setCinematic(true) for this use: it also suppresses unfocused WebUIs,
including your own loading overlay. See WebUI configuration.
Errors and diagnostics
| Symptom | Check |
|---|---|
| No overlay appears | Confirm the resource is loaded, the client supports loadingState, and the teleport opens a native loading cycle. Check Lua/WebUI creation errors and the ui:ready handshake. |
| Your HUD is still over the loading artwork | Hide that resource's WebUI with changed; Open77.hud only controls native HUD claims. |
| Cover stays at 100% | Wait for finished / active == false; progress is not a completion signal. |
| Cover vanishes in cinematic mode | Release the cinematic claim or coordinate with its owner; cinematic mode can hide an unfocused WebUI. |
permission_denied:screen.read: declare the read permission.loading_screen_unavailable: the guarded native observer is unavailable or has not observed the loading layer yet. Do not interpretnilasfalse.loading_screen_unavailable_on_this_host: no client backend on this host.
The local debug command session.loading returns the same state as the Lua
query. There is no arbitrary engine pointer or native-call API exposed to Lua.
// For agents & LLMsthis page as Markdownllms.txtllms-full.txt
