OX_LIBFIVEMLUADEVELOPEROVEREXTENDEDMODULESCALLBACKSZONESSCRIPTING June 7, 2026 · 11 min read

How ox_lib Works Under the Hood

If you’ve touched FiveM development in the last two years, you’ve used ox_lib — or at least used something that depends on it. It’s everywhere. Context menus, notifications, callbacks, zones, progress bars — ox_lib handles all of it through a single, lightweight dependency.

But most developers just drop @ox_lib/init.lua into their fxmanifest and start calling functions without understanding what’s actually happening. I’ve been building scripts on top of ox_lib for years, and knowing how it works under the hood has saved me from countless bugs and performance issues. This is the breakdown I wish I had when I started.

What ox_lib Actually Is

ox_lib is a standalone utility library from the Overextended team. It’s not a framework — it doesn’t care if you run ESX, QBCore, or Qbox. It sits underneath your framework and provides reusable modules: UI components, async callbacks, zone detection, caching, streaming helpers, locale support, and about 30 other things.

When you add this to your fxmanifest:

shared_script '@ox_lib/init.lua'

You get three new globals injected into your resource:

  • lib — the main interface for dynamically importing ox_lib modules
  • require — for importing modules from your own script (not ox_lib’s)
  • cache — a reactive caching layer that tracks player state

That’s it. No bloated initialization, no framework handshake, no waiting for the server to be ready. It just works.

If you’re still building FiveM scripts without understanding these three globals, you should read up on Lua scripting fundamentals first — the concepts here build on that foundation.

The Module System

ox_lib doesn’t load everything at once. It uses a module system where you either declare what you need upfront in your fxmanifest or load modules dynamically at runtime.

Static loading (fxmanifest):

ox_libs {
    'locale',
    'math',
    'table',
}

Dynamic loading (runtime):

-- Loaded on first access automatically
lib.callback.register('myCallback', function(source)
    -- lib.callback gets loaded when you first reference it
end)

The dynamic approach is what most people use without realizing it. When you call lib.zones.box() or lib.inputDialog(), ox_lib loads that module on demand. It’s lazy loading — the module only exists in memory if your script actually uses it.

This matters for performance. If your script only uses notifications and context menus, you’re not paying the memory cost for zones, callbacks, streaming, raycasting, or any of the other 30+ modules. Compare that to older libraries that loaded everything on startup regardless of what you needed.

The Callback System — Why It’s Better

If you’ve worked with ESX, you know ESX.TriggerServerCallback. It works, but the syntax is a nested callback mess:

-- The old ESX way
ESX.TriggerServerCallback('esx:getPlayerData', function(data)
    -- now you're nested inside a callback
    -- good luck chaining three of these together
    print(data.money)
end)

ox_lib gives you two patterns. The traditional callback:

lib.callback('getPlayerMoney', false, function(money)
    print(money)
end)

And the await pattern, which is what you should actually be using:

-- Clean, linear, readable
local money = lib.callback.await('getPlayerMoney', false)
print(money)

The await version yields the current coroutine until the server responds, then returns the value directly. No nesting. No callback hell. You write code that reads top to bottom.

Server-side registration is equally clean:

lib.callback.register('getPlayerMoney', function(source)
    local player = GetPlayerData(source)
    return player.money
end)

This is a direct replacement for ESX.RegisterServerCallback and QBCore.Functions.CreateCallback. If you’re still using those, you’re writing more code for the same result. Understanding how FiveM’s client-server event system works helps you appreciate why ox_lib’s callback abstraction is a significant improvement — it handles the event registration, response routing, and error cases for you.

Zones — PolyZone’s Replacement

PolyZone used to be the go-to for spatial detection. It worked, but it was heavy, and the API was clunky. ox_lib’s zone system is a drop-in replacement that performs better and has a simpler API.

Three zone types:

Box zones:

local zone = lib.zones.box({
    coords = vec3(100.0, 200.0, 30.0),
    size = vec3(10.0, 10.0, 5.0),
    rotation = 45,
    debug = true,
    onEnter = function()
        print('Player entered the zone')
    end,
    onExit = function()
        print('Player left the zone')
    end,
    inside = function()
        -- Runs every frame while inside
        -- Use sparingly
    end
})

Sphere zones:

local zone = lib.zones.sphere({
    coords = vec3(100.0, 200.0, 30.0),
    radius = 5.0,
    debug = true,
    onEnter = function()
        lib.showTextUI('Press [E] to interact')
    end,
    onExit = function()
        lib.hideTextUI()
    end
})

Poly zones:

local zone = lib.zones.poly({
    points = {
        vec3(100.0, 100.0, 30.0),
        vec3(110.0, 100.0, 30.0),
        vec3(110.0, 110.0, 30.0),
        vec3(100.0, 110.0, 30.0),
    },
    thickness = 5.0,
    debug = true,
    onEnter = function()
        print('Inside polygon')
    end
})

A few things worth noting. The inside callback runs every frame — don’t do heavy logic there. Use onEnter to set up state and onExit to tear it down. Also, zones currently have limited server-side support — onEnter, onExit, and inside only work on the client.

You can remove zones dynamically with zone:remove() and toggle debug visualization with zone:setDebug(). The built-in /zone command lets you create zones in-game and export the coordinates, which saves a lot of trial and error.

If you need server-authoritative state rather than client-side spatial detection, zones aren’t the right tool — you’d want state bags or server-side event checks instead.

The Cache System

This one flies under the radar, but it’s one of the most useful parts of ox_lib. The cache global provides reactive access to commonly needed player data without triggering natives every frame.

-- These values are cached and updated automatically
local ped = cache.ped        -- Player ped handle
local veh = cache.vehicle    -- Current vehicle (or nil)
local seat = cache.seat      -- Current seat index
local weapon = cache.weapon  -- Current weapon hash

The key insight: these values are event-driven, not polled. ox_lib listens for state changes and updates the cache when something actually changes. You’re not calling GetPlayerPed(-1) in a loop — the cache already has the current value.

You can also react to changes:

lib.onCache('vehicle', function(vehicle)
    if vehicle then
        print('Entered vehicle:', vehicle)
    else
        print('Left vehicle')
    end
end)

This is significantly more efficient than the pattern I still see in tons of scripts:

-- Don't do this
CreateThread(function()
    while true do
        Wait(500)
        local ped = PlayerPedId()
        local veh = GetVehiclePedIsIn(ped, false)
        if veh ~= 0 then
            -- do stuff
        end
    end
end)

The cache pattern uses zero CPU when nothing changes. The polling pattern wastes cycles every 500ms regardless. If you care about script performance, switching to cache is one of the easiest wins.

The UI Stack

ox_lib’s interface modules cover the majority of in-game UI needs. Here’s what you get and when to use each one.

Context Menus — for multi-option menus with icons, descriptions, and nested submenus:

lib.registerContext({
    id = 'shop_menu',
    title = 'Gun Store',
    options = {
        {
            title = 'Pistol',
            description = '$500',
            icon = 'gun',
            onSelect = function()
                TriggerServerEvent('shop:buy', 'pistol')
            end,
        },
        {
            title = 'Ammo',
            description = '$50 per box',
            icon = 'box',
            onSelect = function()
                TriggerServerEvent('shop:buy', 'ammo')
            end,
        },
    }
})

lib.showContext('shop_menu')

Input Dialogs — for gathering structured player input:

local input = lib.inputDialog('Create Character', {
    { type = 'input', label = 'First Name', required = true },
    { type = 'input', label = 'Last Name', required = true },
    { type = 'number', label = 'Age', min = 18, max = 80 },
    { type = 'select', label = 'Gender', options = {
        { value = 'male', label = 'Male' },
        { value = 'female', label = 'Female' },
    }},
})

if not input then return end -- Player cancelled

local firstName = input[1]
local lastName = input[2]

Always check for nil on input dialog returns. Players cancel dialogs constantly, and an unchecked nil will crash your script.

Progress Bars — for timed actions with animation and prop support:

if lib.progressBar({
    duration = 5000,
    label = 'Cooking...',
    useWhileDead = false,
    canCancel = true,
    anim = {
        dict = 'amb@prop_human_bbq@male@idle_a',
        clip = 'idle_b',
    },
}) then
    -- Completed
    TriggerServerEvent('cooking:finish')
else
    -- Cancelled
    lib.notify({ description = 'Cancelled', type = 'error' })
end

Notifications — clean, customizable alerts:

lib.notify({
    title = 'Success',
    description = 'Item purchased',
    type = 'success',   -- 'success', 'error', 'warning', 'info'
    position = 'top',
    duration = 3000,
})

Skill Checks — QTE-style minigames:

local success = lib.skillCheck({'easy', 'easy', 'medium'}, {'w', 'a', 's', 'd'})

Radial Menus — wheel-style quick-access menus:

lib.addRadialItem({
    id = 'police_menu',
    icon = 'shield',
    label = 'Police Menu',
    onSelect = function()
        -- open police menu
    end
})

The UI components use Mantine under the hood (a React component library), and you can customize colors via convars in your server.cfg:

setr ox:primaryColor blue
setr ox:primaryShade 8

Streaming Module

Loading models, animations, and particle effects is one of those things that seems simple but goes wrong constantly. The native approach requires requesting, waiting, and then remembering to release when done. ox_lib handles all of it:

-- Load a model with automatic cleanup
lib.requestModel('prop_money_bag_01')

-- Load an animation dictionary
lib.requestAnimDict('amb@prop_human_bbq@male@idle_a')

-- Load a particle effect
lib.requestNamedPtfxAsset('core')

These functions are synchronous — they block until the asset is loaded (with a timeout so you don’t freeze forever). They also handle the cleanup so you don’t leak memory from unreleased assets.

Points vs Zones — Know the Difference

ox_lib has both zones and points, and they solve different problems. Zones define areas with boundaries and detect when players enter, exit, or stay inside. Points are just coordinates with a detection radius — they fire when you’re “nearby” and stop when you leave.

local point = lib.points.new({
    coords = vec3(200.0, 200.0, 30.0),
    distance = 10,
    nearby = function(self)
        -- Runs when within distance
        DrawMarker(1, self.coords.x, self.coords.y, self.coords.z,
            0, 0, 0, 0, 0, 0, 1.0, 1.0, 1.0, 255, 0, 0, 100, false)
    end,
})

Points are lighter than zones because they skip the polygon math. If you just need “is the player near this spot,” use a point. If you need “is the player inside this specific area,” use a zone.

Practical Integration Tips

Here’s what I’ve learned from building scripts that depend on ox_lib.

Always check for ox_lib before using it. If your script is meant to work on servers that might not have it:

if not lib then
    print('ox_lib is required')
    return
end

Don’t mix old UI patterns with ox_lib. If you’re using ox_lib notifications, don’t also use QBCore’s notification system. Pick one and stick with it for consistency. All of our scripts — like LMX StoreMaster and LMX RestaurantMaster — use ox_lib exclusively for UI so there’s no framework-specific dependency on the frontend.

Use the ACL module for permissions. Instead of checking player jobs manually, ox_lib’s ACL system lets you define permission groups that work independently of your framework.

The Locale module is underused. If your script will ever be used on non-English servers, wrap your strings in locale() calls from day one. Retrofitting localization is painful.

If you’re building scripts for multiple frameworks — which you should be doing if you want to reach the widest audience — ox_lib is the bridge that makes that possible. Its modules are framework-agnostic, so your UI, callbacks, and zones work the same regardless of the underlying framework.

When Not to Use ox_lib

ox_lib is not always the answer. A few cases where you should skip it:

Simple standalone scripts. If your script is 50 lines and just spawns a ped, adding ox_lib as a dependency is overkill.

Custom UI. If you need a unique, branded interface that doesn’t look like every other ox_lib menu, build your own NUI. ox_lib’s UI is clean but recognizable — players can tell when everything on a server uses the same context menu style.

Server-side zone checks. ox_lib zones are primarily client-side. If you need server-authoritative spatial detection (for anti-cheat, for example), you’ll need a different solution.

Getting Started

If you haven’t set up ox_lib yet, check out our guide on setting up ox_inventory — it walks through ox_lib installation as a prerequisite.

For free scripts that demonstrate ox_lib patterns in practice, browse our free scripts collection. Many of them use ox_lib for UI and callbacks, so you can study real implementations rather than just reading docs.

The official documentation at overextended.dev is the definitive reference, but it’s organized as an API reference rather than a tutorial. Hopefully this breakdown fills the gap between “here are the function signatures” and “here’s how it all fits together.”

If you run into issues or want to see specific ox_lib patterns in action, drop by our Discord. Developers in there work with ox_lib daily and can help debug integration issues.

ox_lib isn’t going anywhere. It’s the standard utility layer for FiveM development, and if you’re serious about building scripts — whether for your own server or for selling on marketplaces — understanding how it works under the hood will make you a better developer. Stop cargo-culting code from tutorials and start understanding what lib actually does when you call it.

YBN
YBN Scripts
FiveM script developer at YBN. Building premium ESX, QBCore & Qbox resources.

Related Posts

Need scripts for your server?

Check out our premium FiveM resources — ESX, QBCore & Qbox supported.

Browse Premium Scripts → Free Scripts →