API Reference

For developers — place your resource's on-screen panels around Oxide HUD with olink.hud.AddPanel, follow its hide reasons, hold the HUD hidden, draw editor previews, use the server accent, and read the lower-level calls, events and exports.

This page is for developers who want their resource's on-screen panels to work with Oxide HUD. Server owners only need Installation, Features and Configuration.

A panel is any part of your resource's page that stays on screen while the player plays: a job timer, a stage card, a row of control hints, a notification area. With one call, olink.hud.AddPanel, Oxide HUD:

  • places the panel clear of its own blocks, the minimap, the server logo and other resources' panels;
  • lets the player move it, hide it while editing, or switch it off in game, in the /hud editor;
  • hides it while the HUD is hidden (pause menu, death, character select, a cutscene);
  • gives it the server's accent colour.

Your resource never depends on Oxide HUD. Everything goes through the client hud namespace of o-link (1.12.0 or newer). On a server without Oxide HUD, or with an older o-link, your panel stays exactly where your page has always drawn it.

o-link HUD panels guide

The o-link HUD panels guide covers the same API with more patterns: growing stacks, panels drawn with natives, and default spots at the bottom centre.

Your First Panel

A complete resource with one panel: three files.

fxmanifest.lua

fx_version 'cerulean'
game 'gta5'
lua54 'yes'

client_script 'client.lua'
ui_page 'web/index.html'
files { 'web/index.html' }

dependency 'o-link'

client.lua

olink = exports['o-link']:olink()

-- The page calls this once it is listening, so it never misses the first message.
RegisterNUICallback('pageReady', function(_, cb)
    cb('ok')
    if not (olink.hud and olink.hud.AddPanel) then return end -- an o-link without HUD support

    olink.hud.AddPanel({
        id = 'my-job:timer',       -- unique: start it with your resource name
        label = 'My job: timer',   -- shown on the panel in the /hud editor
        anchor = 'top-right',      -- the corner or edge the panel is pinned to
        size = { w = 24, h = 8 },  -- in vh: percent of the screen height
        offset = { x = 3, y = 4 }, -- where the page draws it without a HUD, in vh from the anchor
    }, function(panel)
        SendNUIMessage({ action = 'hudPanel', panel = panel })
    end)
end)

web/index.html

<style>
  /* Where the panel sits without a HUD. Keep it in a stylesheet: the HUD's placement is set
     inline, and when it is cleared your own position comes back. */
  #timer { position: fixed; right: 3vh; top: 4vh; width: 24vh; height: 8vh; }
</style>

<div id="timer">4:32</div>

<script>
  const timer = document.getElementById('timer')

  window.addEventListener('message', (event) => {
    if (event.data.action !== 'hudPanel') return
    const panel = event.data.panel
    Object.assign(timer.style, panel.style)          // where the HUD put it
    timer.style.display = panel.show ? '' : 'none'   // switched off, or hidden with the HUD
  })

  fetch(`https://${GetParentResourceName()}/pageReady`, { method: 'POST', body: '{}' })
</script>

That is the whole integration:

  1. The page loads and calls pageReady, and Lua adds the panel. (FiveM drops NUI messages sent before a page is listening, so always add panels from a callback your page calls.)
  2. o-link calls your function straight away, and again every time something about the panel changes: the player drags it, switches it off, opens the editor, the HUD hides or comes back, the HUD restarts. Your function runs in your resource, so SendNUIMessage reaches your page.
  3. The page applies panel.style and panel.show.

In a Vue page the same two fields bind directly: <div v-if="active && panel.show" :style="panel.style">.

AddPanel returns true when the panel was added. When the definition is invalid it returns false and o-link prints a line in the F8 console saying what was wrong. Calling it again with the same id replaces the panel. olink.hud.RemovePanel(id) removes it, and every panel is removed when your resource stops.

What Your Function Receives

FieldTypeMeaning
idstringThe panel's id, so one function can serve several panels
hostedbooleantrue while Oxide HUD places the panel. false without it: draw it where you always do
showbooleanfalse while the panel must not be drawn: the player switched it off, the editor leaves it out, or the HUD is hidden for a reason the panel follows. Draw your panel only while it is true
previewbooleantrue while the /hud editor is open and shows this panel. Draw sample content if nothing real is up
styletableCSS for the panel's root element: left, right, top, bottom and opacity, as strings. Apply all of them every time. Empty strings hand the position back to your stylesheet
themetable or nilThe server's accent colours (The Server Accent). nil keeps your own colours
positiontable or nilThe raw placement in vh (anchor, x, y, left, top, w, h, hidden, hiddenBy), for panels drawn with natives instead of a page. nil while not hosted

Panel Options

Only id and label are required.

OptionDefaultWhat it does
idrequired1 to 64 characters of A-Z a-z 0-9 _ - . :. Start it with your resource name, for example 'my-job:timer'
labelrequiredThe panel's name in the /hud editor. Pass translated text
anchor'top-left'Where the panel is pinned: top-left, top, top-right, left, center, right, bottom-left, bottom, bottom-right. Any other value makes the call fail
size{ w = 20, h = 10 }The panel's size in vh. A missing or non-positive width becomes 20 and height 10. Width is capped at 400 and height at 100
offsetnone{ x, y } in vh, measured in from the anchor's edges: where your page draws the panel today. Oxide HUD starts there and moves the panel only as far as it must. Without it, the HUD finds a free spot
overlayfalsetrue for a panel that only shows briefly (a prompt, a progress bar, a tutorial card): it keeps clear of the HUD, but other panels do not move for it
essentialfalsetrue for a panel the player must not miss (notifications, a downed card, alerts): it can be hidden while editing but never switched off in game
shareSpotnoneA name in the same characters as id, for panels that never show at the same time, such as the stage cards of different jobs. Panels with the same shareSpot and anchor share one spot instead of stacking
hideWith{ 'unloaded', 'external', 'pause', 'dead' }The hide reasons this panel follows. false never hides with the HUD
accenttruefalse never sends theme. Give server owners a config switch for it

Units. Every size and offset is in vh, percent of the screen height, so a panel keeps its place at every resolution. The screen is 100 * width / height vh wide (about 177.8 at 16:9) and 100 vh tall; 3 vh is 32 px at 1080p. If your panel is sized in pixels, convert at registration: local _, height = GetActiveScreenResolution(), then w = 300 * 100 / height.

Anchors. The anchor you register never changes, wherever the player drags the panel; only the distances change. On a centred anchor (top, bottom, left, right, center) the distance is measured from the centre line to the middle of the panel, so centre your element on it yourself, for example with transform: translateX(-50%) for top and bottom. style never sets transform, so your own transforms and transitions keep working.

Shared spots in use: job-timer (top: gig job timer cards), drug-stage (bottom: drug lab stage cards), controls-hint (bottom: rows of key prompts), activity:left and activity:right (top corners: activity cards). Use one when your panel fits it; a new name needs no change in Oxide HUD.

Hiding with the HUD

While the HUD is hidden, the reason is the first of these that applies:

ReasonWhen
unloadedNo character: at character select, after logout, before the first character loads
externalA resource holds the HUD hidden with SetVisible(false): a full-screen view or a cinematic
pauseThe pause menu is open
deadThe player is dead or downed
phoneOxide Phone is open
focusAny NUI has focus (a menu, an inventory), unless a resource holds KeepVisible

show turns false only for the reasons listed in your panel's hideWith. Follow only the reasons that suit your panel: a downed card must not hide for dead, and a panel whose own menu takes focus must not hide for focus (the default leaves phone and focus out). Because pause comes before dead, a downed card can still hide for the pause menu.

The HUD stays on screen while the /hud editor is open, even though the editor takes NUI focus.

Hiding the HUD

olink.hud.SetVisible(false)   -- hold the HUD hidden for your resource
olink.hud.SetVisible(true)    -- release your hold

Hold it for a full-screen view that runs without NUI focus, such as a bodycam, CCTV or a camera scene, and release it on every path that closes the view. A menu with NUI focus needs nothing: the HUD already hides for focus.

olink.hud.KeepVisible(true)    -- your NUI focus is not a menu: keep the HUD up
olink.hud.KeepVisible(false)   -- that focus ended

Use it when your resource takes NUI focus while the player keeps playing: a chat input, a remote used while driving. While any resource keeps the HUD up, NUI focus does not hide it; the pause menu, death and SetVisible(false) still do.

Both return true when Oxide HUD took the request and false without it. The HUD stays hidden while any resource holds it hidden. Each hold ends when its resource stops, and o-link remembers it while Oxide HUD is stopped and applies it again when Oxide HUD starts. With several such views in one resource, release only when the last one closes.

Previews in the Editor

While the /hud editor is open, Oxide HUD draws a labelled placeholder for your panel and sets preview = true. Draw your panel with still sample content when nothing real is up, so the player sees what they are placing; if the real panel is up, draw that instead of both.

  • show and preview already follow the editor: the player's eye, Solo, and shared spots (the editor shows one panel of a shared spot at a time).
  • A panel the player switched off in game is still previewed, at the opacity style gives it (0.45), as its placeholder is marked Hidden in game.
  • While the player drags the placeholder, your function is called about ten times a second, so the preview moves with it.
  • Keep the preview display only: no NUI focus, no input, no sounds, nothing sent to the server. Do not raise your page (SetNuiZindex) while the editor is open; the editor's controls must stay on top.
  • Size it in vh (or in pixels scaled to the screen height) and register the same size, so it fits its placeholder at every resolution.

The Server Accent

A server owner sets one accent colour in Oxide HUD's config.lua (Config.Theme.accent, see Configuration). Your panel gets it as panel.theme:

FieldUse
accentWhere your panel uses its own accent today: bars, fills, borders, icons, glows
lightAccent-coloured text and highlights
darkGradient ends, deep fills, pressed states
contrast'#ffffff' or '#0f1113': text and icons drawn on a solid accent fill
lockedtrue when the owner set Config.Theme.lockAccent: players may not replace the accent with colours of their own

The four colours are lowercase '#rrggbb' strings, all worked out from the one colour the owner set:

  • accent is that colour, lightened only when it is too dark to read on a dark panel (until it reaches 3:1 contrast against #0f1113);
  • light mixes accent toward white, at least 30% and further until it reaches 4.5:1 against #0f1113;
  • dark mixes accent 35% toward black;
  • contrast is white unless white text on accent would fall under 3:1.

theme is nil when the owner set no accent, while Oxide HUD is stopped, without Oxide HUD, and when you pass accent = false. In every one of those cases, keep your own colours. In your page, override your own accent CSS variables with the theme's values, and remove them again when it is nil:

const VARS = { '--my-accent': 'accent', '--my-accent-text': 'light', '--my-on-accent': 'contrast' }
function setTheme(theme) {
  const root = document.documentElement.style
  for (const [name, key] of Object.entries(VARS)) {
    if (theme && /^#[0-9a-f]{6}$/i.test(theme[key])) root.setProperty(name, theme[key])
    else root.removeProperty(name)
  }
}
  • Replace only your accent. Colours that carry meaning (good, warning and bad states, notification types) keep their colours.
  • Give server owners a switch, such as Config.UseHudAccent = true, and pass accent = Config.UseHudAccent ~= false.
  • If your panel lets players pick colours, hide that choice while locked is true.

olink.hud.GetTheme() returns the same table (or nil) for resources that colour something other than a panel. A player's own accent from the editor's Theme tab colours only their own HUD, never other resources' panels.

How Oxide HUD Places Panels

Separately for the on-foot and in-vehicle layouts:

  1. Panels the player moved sit exactly where they were put. If your resource later registers the panel with a different anchor, that saved spot is ignored.
  2. Every other panel starts at its offset (without one, 1 vh inside GTA's safe zone at its anchor) and moves away from its anchor's edge (down for the top and middle rows, up for the bottom row) to the first spot clear of everything it keeps clear of. A panel on the top or bottom anchor only looks on its own half of the screen, so it never lands in the middle of the player's view; a corner or side panel can use its whole side.
  3. When the column is full, the panel takes the spot on its own half of the screen, still clear of the HUD's own parts, that covers the least of other panels. It covers essential panels only when every spot would. If not even the HUD leaves room there (a panel taller than the space above the vehicle cluster), it stays at its offset. A panel is never pushed onto the HUD or clamped at the far edge of the screen.
  4. What a panel keeps clear of: the HUD's blocks and groups, each at its full size, so a panel does not move when a block hides (decor such as background panels, lines and brackets does not count); the minimap while it is shown; the server logo; the areas the owner lists in Config.ReservedAreas; and every non-overlay panel placed before it that the player has not switched off. Things are kept 1 vh apart.
  5. Panels against the edge. A panel at offset y = 0 on a top or bottom anchor (a card that peeks up from the bottom) keeps clear only of what the HUD draws right now, not of room kept for hidden blocks, and sits flush against what it has to move past. So it stays against the edge, or rests directly on the speedometer in a vehicle.
  6. Order. Panels the player has not moved claim their spots in the same order every session, whatever order resources started in: essential panels first, then the panel whose offset asks to sit nearest its anchor's edge, then by id (a shared spot by its name).
  7. Overlays keep clear of the HUD's own parts but not of other panels, and no panel moves for them.
  8. Shared spots are placed once, as a box as large as the largest member, and keep space at the size of the members that take space, so a short-lived overlay member does not push other panels further. Dragging one in the editor moves them all.

What this means for your panel: give the offset you want, nearest the edge first; mark essential only for what the player must not miss; mark anything that shows briefly as overlay; and register the size your panel draws at.

Switching between on foot and in a vehicle places every panel again, and Reset in the editor clears the player's moves for the layout being edited.

Panels the Player Hides

In the editor's Layers tab, under Other resources, the player can hide a panel (or every panel of a resource) while editing, Solo one resource, or switch a panel off in game with Show in game. Show in game is not offered for an essential panel.

With AddPanel you only need show: it is false whenever the panel must not be drawn. With the lower-level calls, a position carries hidden = true and hiddenBy = 'player' (switched off in game) or 'editor' (left out while the editor is open; this comes first while it is open). Test pos.hidden == true, since positions from a server without Oxide HUD have no hidden field.

A panel switched off in game stops taking space, so panels placed after it may move into its spot. A panel hidden only while editing keeps its space.

Without Oxide HUD

When Oxide HUD is not installed, o-link's _default provider answers the same calls:

  • AddPanel calls your function once with hosted = false, empty style values and show = true, so your page keeps its own position. If Oxide HUD starts later, o-link adds the panel to it and calls your function again.
  • A panel registered with an offset sits there (kept on screen); one without is placed inside GTA's safe zone, clear of estimated top-corner HUDs and the minimap.
  • essential and shareSpot are accepted and ignored.
  • SetVisible and KeepVisible return false, IsVisible is always true, GetTheme returns nil, and no olink:client:hud:* event fires.

On an o-link older than 1.12.0, olink.hud itself is nil, so always check it (if olink.hud and olink.hud.AddPanel then). While Oxide HUD is installed but stopped, every call answers as _default, except that SetVisible and KeepVisible holds are remembered and applied when it starts.

Resources that start before o-link (a notification or progress bar resource) cannot keep a reference from load time. Look the namespace up at call time, and add your panels from your page-ready callback and again on olink:client:hud:ready:

local function hudNamespace()
    if GetResourceState('o-link') ~= 'started' then return nil end
    local ok, olink = pcall(function() return exports['o-link']:olink() end)
    return ok and olink and olink.hud or nil
end

Lower-Level Calls

AddPanel is built on these. Use them only to lay out panels yourself, for example a panel drawn with natives.

local position = olink.hud.RegisterWidget(def)   -- same fields as AddPanel, without hideWith and accent
olink.hud.UnregisterWidget(id)                   -- boolean: false if the id was not registered
olink.hud.GetWidgetPosition(id)                  -- position, or nil if the id is not registered
local visible, reason = olink.hud.IsVisible()    -- boolean, string|nil
local layout = olink.hud.GetReservedAreas()      -- what the HUD covers, see below

RegisterWidget returns a position, or false when def is invalid; Oxide HUD then prints one line in the F8 console naming the panel, the resource and what was wrong. Register at your own start and again on olink:client:hud:ready, and read IsVisible() each time, because events only report changes.

Position fieldTypeMeaning
idstringThe panel's id
anchorstringThe anchor you registered
x, ynumberOffsets to draw the panel at, in vh from the anchor
w, hnumberThe panel's size in vh
left, topnumberThe panel's top-left corner, in vh from the screen's top-left corner
movedbooleantrue when the player placed the panel in the editor
hidden, hiddenByboolean, string or nilSee Panels the Player Hides

The position RegisterWidget returns is the saved or requested spot until Oxide HUD's page has measured its own blocks; the placed one follows through olink:client:hud:layoutChanged. Always apply both.

These local client events fire on the client (AddEventHandler, not RegisterNetEvent):

EventArgsWhen
olink:client:hud:readyThe HUD is up for the character. Register (again) here
olink:client:hud:layoutChanged(positions)Something moved or a panel was hidden or shown. positions maps every registered id to its position; read your own ids
olink:client:hud:visibilityChanged(visible, reason)The HUD was hidden or shown. reason is nil while visible
olink:client:hud:editMode(enabled)The /hud editor opened or closed. Positions are relayed just before it as the editor opens and just after it as the editor closes
olink:client:hud:themeChanged(theme)Oxide HUD started (the theme) or stopped (nil), only when the server set an accent

When Oxide HUD stops, it fires visibilityChanged(true, nil) if it was hidden and editMode(false) if the editor was open, so panels following it never stay hidden or in preview.

GetReservedAreas() returns, in vh from the screen's top-left corner:

{
    measured = true,                  -- true once a HUD has measured its own layout
    screen = { w, h, pixelWidth, pixelHeight },
    safeZone = { x, y, w, h },        -- GTA's safe zone
    areas = {
        { id, kind, x, y, w, h, resource? },   -- kind: widget, minimap, logo, reserved, panel, overlay
    },
}

Oxide HUD returns measured = false and no areas until its page has measured its layout, a moment after it loads; call it again after each layoutChanged. _default returns measured = false and areas of kind estimate.

Exports

Oxide HUD's client exports exist for o-link's adapter. Use olink.hud instead: it works without Oxide HUD and records the calling resource for you.

ExportUsed by olink.hud for
RegisterPanel(def)RegisterWidget and AddPanel. def.resource sets the owner; without it, the calling resource
UnregisterPanel(id)UnregisterWidget and RemovePanel
GetPanelPosition(id)GetWidgetPosition
GetReservedAreas()GetReservedAreas
SetHiddenBy(owner, hidden)SetVisible
SetKeepVisible(owner, keep)KeepVisible
GetVisibility()IsVisible. Returns { visible, reason }
GetTheme()GetTheme. Returns the theme table or nil

Data Oxide HUD Reads

Oxide HUD reads all game data through o-link, so a framework or resource shows up on the HUD as soon as o-link supports it. To make your seatbelt, fuel or voice resource appear on the HUD, add an o-link adapter for it; nothing in Oxide HUD changes.

HUD datao-link (client)
Character loaded / unloadedolink:client:playerReady, olink:client:playerUnload, olink:client:startCharacterSelect; olink.framework.GetName(), olink.framework.GetIsPlayerLoaded()
Hunger, thirst, stressolink.needs.GetAll(), olink:client:needsChanged
Cash, bankolink.money.GetBalance('cash' | 'bank'), olink:client:moneyChanged
Character nameolink.character.GetName()
Dead, downedolink.death.IsPlayerDead(), olink.death.IsPlayerDowned(), olink:client:playerDeathStateChanged
Fuelolink.fuel.GetFuel(vehicle)
Seatbeltolink.seatbelt.IsOn()
Voice range, talking, radioolink.voice.GetState()

Health, armour, oxygen, stamina, vehicle, aircraft and boat data, street and area names, heading, the clock, the server ID and the equipped weapon are read from GTA natives.

Next Steps