Skip to main content

JavaScript SDK

A custom app or widget runs in an iframe. Reaching its own Lua works without RoadVR, and being reached from its own Lua works through sendToRoadVrApp. The SDK is the third direction: the page asking RoadVR itself what is going on, and being told when it changes.

Loading it

The path is relative to the RoadVR resource, so public/ belongs in it. There is nothing to add to your own files {} block — the file is served by RoadVR, which means there is no copy in your resource that can go stale against a newer headset.
Use the https://cfx-nui- form, not nui://. Both reach the same file, but the address decides what your page is standing on: a browser sends a Referer only from an http(s) document, so a page loaded over nui:// reaches every service on the web without one — and a service that checks it, YouTube being the loud example, refuses to serve you. The cfx-nui- form gives your page a real web origin, which is also what makes fetch and browser storage behave the way you expect.
Opened straight in a browser while you work on the page, the cfx-nui- address does not resolve at all and window.RoadVr is simply not there. Guard for it so the rest of your page still runs:
The global is RoadVr, matching the resource name. The product is written RoadVR; the identifier is not.

Getting started

RoadVr.ready()

Promise<context>
Settles once the headset has answered, with the context object below.
Inside FiveM but with no headset on, nothing answers. After two seconds it settles anyway, with surface: null and usable defaults — rather than leaving your page waiting forever. Calling it twice is free: once it has settled it resolves immediately.

RoadVr.context

The same object ready() resolves with, kept up to date. Events update it before the handlers run, so a handler that would rather read the whole picture finds it already current — with two exceptions: placement does not write window.distance, and headset.state keeps the value it had at ready().
string | null
The SDK contract version the headset speaks, not the resource version. It moves when a field is added, so you can check whether the headset you happen to be running in knows about something. null means nothing answered.RoadVr.version, on the global, is the version of the script file you loaded.
string | null
'app', 'widget', or null outside a headset.
object | null
Apps only. width and height are metres, not pixels — your page always lays out 1280 CSS pixels wide however large the window stands in the world, and 1280 × height / width tall. Reading these as pixels gives you a layout that changes with window size.anchor is 'body' (follows the player), 'world' (stays put) or 'group' (the player’s windows are following as a group).
object | null
Widgets only. size is metres of width — one number, because the height follows from aspect. mount is 'recess' or 'raised'.
object
light is a boolean, accent a hex colour, iconTheme the icon set name. palette is RoadVR’s own CSS custom properties — see applyTheme.
object
language is the player’s own interface choice, not the server’s Config.Locale. Those are two different settings, and mixing them up shows German text to a player who set English.
object
state is off, passthrough, focus or shutdown, as it was when ready() settled. focus is the mode in which the player can actually click things.focused is this window being the focused one, not some window being focused.visible is false while the pause menu or the big map is up, the camera is out of first person, the drone is flying, or the cinema screen is up. One field for all of them, because the answer is the same in all of them: stop animating.
object | null
null until you call watch('world'). See Weather and the clock.

RoadVr.on()

string
required
One of the events below.
function
required
Receives the changed subtree, not the whole context.
function
The counterpart. Calling it removes this one handler.

The events

close is the one to wire up first. It is the only warning your page gets that it is about to be taken down, and it is sent before the frame is detached — so whatever has to survive being closed gets written there.

message and the raw listener

RoadVr.on('message', …) and a plain window.addEventListener('message', …) carry the same pushes from your own Lua. The difference is that the SDK keeps its own traffic out of the first one; a raw listener sees both and has to tell them apart itself. Everything RoadVR forwards carries source: 'roadvr'. SDK traffic additionally carries channel: 'sdk'.

RoadVr.applyTheme()

HTMLElement
default:"document.documentElement"
Where to write the custom properties.
Writes RoadVR’s palette into your page and keeps it there when the player switches theme — call it once, after ready(). The names are the ones the interface uses itself: --vr-text, --vr-text-dim, --vr-text-3, --vr-glass-top, --vr-glass-bottom, --vr-chrome-top, --vr-chrome-bottom, --vr-separator, --vr-stroke, --fill-idle, --fill-hover, --fill-selected, --on-selected, --view-recessed, --view-thin, --view-regular, --view-thick, plus --vr-accent. Point your own variables at them with a fallback and the page still opens in a browser:

Watched topics

Two things are not sent unless you ask for them, because both tick and a ticker running for a page that never reads it is load nobody chose. Each one starts with its first watcher and stops with its last.

Weather and the clock

Once a second. The world object carries weather, temperature, hours, minutes, windSpeed, windBearing, sunrise, sunset, day, month, year and weekday. RoadVr.context.world is null until you watch it.

How far away your window is

Apps only — a widget hangs in a wall and does not move.
distance is metres from the player’s head to your window, four times a second and only while it actually changes. It is the value to turn a volume down with as the player walks away, and what a display carrying too much detail to read from across the room can simplify itself on.
Nothing else about where anybody is standing travels — not the window’s position and not the player’s. One number is the whole answer to “am I near it”; the position would be a different question with a different answer.
A frame that goes away without calling unwatch still stops counting — a ticker does not stay on for a window nobody can see.

RoadVr.setActions()

Puts buttons in your own window’s ornament — the bar along the bottom edge that already holds the close button, the grabber and the tilt. Yours go to the left of the close button.
array
required
Up to three items of { id, label, icon, onSelect }. An empty array takes them away again.
That is the place for a control that has to stay reachable while your page cannot offer one — over a video, over a map, over anything that swallows pointer events.
string
Yours. It comes back as the payload of the action event, and it is how the SDK finds your onSelect again.
string
Finished text, not a locale key. Your page carries its own strings and RoadVR does not translate them — switch them yourself on the locale event and call setActions again.
string
Either a name from RoadVR’s own bundled set, spelled solar:alt-arrow-left-bold, or an SVG path in a 24×24 box. A name gets you the same filled glyph the close button beside it uses, but only for the icons RoadVR happens to bundle — a name it does not have logs a warning and draws nothing. A path always works and is drawn as an outline.
function
Runs in your page. The press travels as the id and the SDK finds your function again, so a second setActions with a shorter list cannot leave a handler behind for a button that is gone.
Three at most, and apps only. The ornament also holds the window’s own controls, and a fourth button would push those off the bar — so anything past the third is dropped without a word. An item without an id gets its index in the list as its id. A widget has no ornament — it hangs in a wall.

RoadVr.close()

Closes your own window.
The window is resolved from the frame the call came out of, never from something you pass in, so one app cannot close another’s window.
It does nothing for a widget. A widget hangs in a wall and belongs to the player; taking one off is theirs to do.

RoadVr.usePointer()

Who draws the pointer inside your page.
Mouse events do not cross a frame boundary: while the mouse is inside your page the headset hears nothing from it, so it takes its own dot away rather than leave one standing still at your window’s edge. The SDK draws that dot inside your page instead, and hides the system cursor itself as it does.
boolean
default:"true"
false hands the cursor back to your page — for a crosshair, a brush, a game, anything that draws a pointer of its own.
Do not set cursor: none in your CSS instead. Without the SDK loaded the system cursor is all your players have to aim with, and hiding it leaves them aiming blind.

What the SDK cannot do

By design, this version is read plus events, plus three actions — closing your own window, hanging buttons in its ornament, and handing back the pointer. There is no way to play a sound, resize your own window, or open another app from the page. A notification goes through your own Lua and showRoadVrNotification. Nor does the SDK hand out game data — vehicle, health, position and the rest belong in your own Lua, and RoadVR is not a general game API. The message protocol is built so those can be added later without breaking anything already written against it.
A game connects through a different SDK. RoadVrGame covers the shared room, the state every participant sees and the actions your page submits for judgement — see the Game SDK.

Custom Apps

Getting your page onto the home screen

Custom Widgets

Getting your page onto a wall

Game SDK

The other browser API, for multiplayer games

Client API

The Lua half