Skip to main content

Custom Apps

Write your own resource, register an app, and it appears on the headset’s home screen. Opening it loads your page into a window. RoadVR is not modified in the process, so an update overwrites nothing of yours.

Working examples

Two complete, running app resources ship in examples/: roadvr_exampleapp, which reads the game through its own Lua, and roadvr_mediaapp — the Media app itself, which needs almost no Lua and does everything through the SDK instead.

Why a page and not a component

RoadVR’s interface is one compiled Vue bundle. Your resource cannot add a component to a build that shipped before your resource existed — so your app is loaded as a page in a frame instead. That is the better deal anyway. Inside the frame you use whatever you like, plain HTML or a framework, and a crash in your app cannot take the headset down with it.

The three exports

registerRoadVrApp

string
required
Unique across the whole server. Prefix it with your resource name.
string
required
A file in your resource, e.g. ui/index.html. The full address — https://cfx-nui-<your-resource>/<file> — is built for you, so it cannot point somewhere else by accident. Put the file in your own files {} block.
string
Shown under the icon. Defaults to the id.
string | table
A file in your resource, e.g. ui/icon.png — or a pair of tones, see Two icons below.
table
default:"{ w = 1.2, h = 0.75 }"
Window size in metres. Not pixels — see Writing the page.
boolean
default:"false"
Keep it off the home screen and open it yourself with openRoadVrPanel(id).
boolean
false when the id already belongs to another resource — nobody gets to replace someone else’s window.
string
Why it failed, when it did.

Two icons, if you want the setting to work

icon may be a pair instead of one file:
Settings has an icon style — dark, light, or following the interface theme — and every built-in app ships both tones for it. One file name is fine and is what most apps want; it simply shows under either setting. A pair makes your icon follow the player’s choice the same way the rest of the home screen does. The recipe the built-in set uses: dark is a near-black tile with the glyph in the app’s own colour, light is a tile carrying that colour as a gradient with a white glyph.
Name both files in your files {} block. A file RoadVR asks for that is not listed is a missing icon and nothing that says so.
Give only one side of the pair and the other falls back to it, so half a pair is still an icon under both settings rather than a gap under one.

sendToRoadVrApp

boolean
false while the window is closed. That is normal, not an error — your resource has no idea whether the player has your app open.

Building the resource

1

Declare your files

fxmanifest.lua
2

Register on start

client.lua
3

Start it

server.cfg
Put the headset on and your icon is on the home screen.

Talking to your own Lua

Nothing is needed from RoadVR for this. FiveM routes https://<your-resource>/<callback> by resource name, no matter which page fires the request, so inside the frame a plain fetch reaches your own RegisterNUICallback:
The one direction you cannot do alone is inbound. Your own SendNUIMessage would go to your own frame, and that one is never rendered. Use sendToRoadVrApp instead.

Talking to RoadVR itself

Your own Lua tells you about your world. The SDK tells you about the headset: which theme is on, which language the player picked, how big your window is, whether it is focused — and, the one that matters most, that it is about to close.
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, so there is no copy in your resource that can go stale.

Full SDK reference

Every field, every event, every function

The events, briefly

Every on() returns its counterpart:

Two things worth knowing about

watch('placement') tells you how far your window is standing from the player’s head, in metres, four times a second and only while it changes. It is what the Media app turns its volume down with as somebody walks away.
setActions() hangs up to three buttons in your window’s ornament, left of the close button — the place for a control that has to stay reachable while your page cannot offer one, over a video or a map.
The SDK is optional. A page that only wants pushes from its own Lua can keep using a raw listener — everything RoadVR forwards carries source: 'roadvr':

Writing the page

The window renders at a fixed 1280×800 CSS pixels whatever its size in metres. Sizes look oversized in a browser on purpose; that is the same scale every built-in app uses.
RoadVR’s glass sits behind your page. A background colour on body shows up as a rectangle inside the rounded window.
The headset draws its own pointer, and the system cursor would appear next to it.
Set overflow: hidden and make your page fit its window. A scrollbar down the side of a floating pane looks like a fault, and the wheel is taken anyway — it moves the window closer and further away.
RoadVR sets user-select: none on its own panels, but that rule stops at the frame boundary. Your page is its own document and starts from the default, which is selectable — so dragging across your window paints it blue while the headset draws its own pointer next to the smear.RoadVR cannot do this for you. Set it yourself:
Point your own variables at RoadVR’s, with a fallback so the page still opens in a browser:
Then call RoadVr.applyTheme() once and the page follows the player switching theme, without knowing a single colour value.

Lifecycle

Register once at resource start. There is no need to unregister on stop — RoadVR notices your resource going away, removes the app and closes its window. Registration survives the headset being taken off and put back on. The list is kept on the Lua side and pushed to the interface again on every boot, so a resource that started long before anyone put a headset on still shows up.

Trying the examples

Put the headset on and the Example icon is on the home screen. It shows your street, health and armour on request, receives a speed reading pushed once a second, honks the horn of the vehicle you are in — and, in its second column, reports everything the headset tells it through the SDK.
The Media app, as an ordinary resource of its own. It is the other shape: almost no Lua at all — one registerRoadVrApp call and nothing else — with every runtime detail coming through the SDK instead. It is also the one to read for watch('placement') and setActions(), both of which it uses for real.
roadvr_mediaapp registers under the id media, which is the id the built-in Media app used. Starting it takes that place over rather than adding a second icon, so openRoadVrPanel('media') keeps working either way.