What is a Target System and Why Use One?
A target system, sometimes called an eye-target or interaction system, replaces the traditional approach of pressing a key while near something. Instead of walking up to an NPC and pressing E hoping you are close enough, players hold a key (usually left alt) to enter targeting mode, which displays a small crosshair in the center of the screen. When the crosshair hovers over a targetable entity, object, or zone, a menu of available actions appears. This approach is dramatically more intuitive because it gives players precise control over what they interact with. In a crowded scene with multiple NPCs, vehicles, and objects, a target system lets you pick exactly the one you want without wrestling with overlapping interaction prompts. The two most popular implementations are ox_target (from the Overextended team) and qb-target (built for QBCore). This guide covers both, with emphasis on ox_target as it is framework-agnostic and more actively developed.
ox_target vs qb-target: Which to Choose
Both systems accomplish the same goal but differ in API design and performance. ox_target is standalone, works with any framework, uses raycasting for entity detection, and has a cleaner API with better TypeScript support. qb-target is tightly integrated with QBCore, uses a similar raycast approach, and has been around longer so more existing scripts support it out of the box. If you are starting a new server or using ESX/standalone, use ox_target. If you are on QBCore and most of your purchased scripts use qb-target exports, sticking with qb-target avoids compatibility headaches. The good news is that ox_target includes a qb-target compatibility layer, so scripts that call exports['qb-target'] will work with ox_target installed. The reverse is not true, however, so scripts written for ox_target will not work with qb-target without modification.
Adding Targets to Specific Entities
Entity targets attach interaction options to a specific spawned entity like a ped, vehicle, or object. This is useful when you create an NPC for a job and want players to interact specifically with that NPC rather than any ped model in the world. The target follows the entity, so if the NPC walks around, the interaction point moves with them. You provide the entity handle, the options array, and an optional distance that controls how far away the crosshair can detect the entity. Always clean up entity targets when the entity is deleted or the resource stops to prevent orphaned targets from lingering in memory.
-- client.lua: Entity targeting with ox_target
-- Create a shop NPC and add target
local model = joaat('a_m_m_business_01')
lib.requestModel(model)
local ped = CreatePed(0, model, 25.7, -1347.3, 29.5, 270.0, false, false)
FreezeEntityPosition(ped, true)
SetEntityInvincible(ped, true)
SetBlockingOfNonTemporaryEvents(ped, true)
-- Add target to this specific ped
exports.ox_target:addLocalEntity(ped, {
{
name = 'open_shop',
icon = 'fas fa-store',
label = 'Open General Store',
distance = 2.5,
onSelect = function(data)
-- data.entity contains the entity handle
TriggerEvent('shop:open', 'general')
end,
},
{
name = 'talk_to_clerk',
icon = 'fas fa-comment',
label = 'Talk to Clerk',
distance = 2.0,
canInteract = function(entity, distance, coords, name)
-- Only show if player has completed a quest
return PlayerData.questComplete == true
end,
onSelect = function()
TriggerEvent('dialogue:start', 'clerk_01')
end,
},
})
-- Clean up on resource stop
AddEventHandler('onResourceStop', function(resource)
if resource == GetCurrentResourceName() then
exports.ox_target:removeLocalEntity(ped)
DeleteEntity(ped)
end
end)
Model-Based Targets: Targeting All Instances
Model targets apply interaction options to every instance of a specific model in the game world. This is extremely powerful for things like ATMs, vending machines, dumpsters, trash cans, or any prop that exists in multiple locations across the map. Instead of manually registering targets at hundreds of coordinates, you register once by model name and every matching object becomes interactive. The performance impact is minimal because the target system only checks models within the raycast range, not every object in the entire world. Be careful with common models though. Targeting every prop_bin_01a on the map gives players hundreds of interaction points, so make sure the action makes sense for all instances of that model.
-- client.lua: Model-based targeting
-- ox_target: Target all ATM models
exports.ox_target:addModel({'prop_atm_01', 'prop_atm_02', 'prop_atm_03', 'prop_fleeca_atm'}, {
{
name = 'use_atm',
icon = 'fas fa-credit-card',
label = 'Use ATM',
distance = 1.5,
onSelect = function(data)
TriggerEvent('banking:openATM')
end,
},
})
-- Target all vending machines
exports.ox_target:addModel({
'prop_vend_coffe_01',
'prop_vend_soda_01',
'prop_vend_soda_02',
'prop_vend_water_01',
}, {
{
name = 'buy_drink',
icon = 'fas fa-mug-hot',
label = 'Buy Drink ($5)',
distance = 1.5,
onSelect = function(data)
local model = GetEntityModel(data.entity)
TriggerServerEvent('vending:buy', model)
end,
},
})
-- qb-target equivalent for comparison
exports['qb-target']:AddTargetModel({'prop_atm_01', 'prop_atm_02'}, {
options = {
{
type = 'client',
event = 'banking:openATM',
icon = 'fas fa-credit-card',
label = 'Use ATM',
},
},
distance = 1.5,
})
Zone-Based Targets: Invisible Interaction Areas
Zone targets create invisible interaction areas at specific coordinates. These are ideal for places where no physical object exists but you still want players to interact, like a counter in a building interior, a specific spot on the ground for a stash, or an area in front of a door for a lockpick option. You define zones as boxes or spheres with coordinates, size, and rotation. The zone is only active when the crosshair is pointing at the zone's area, making it precise and preventing accidental interactions. Zone targets are also the go-to solution for adding interactions to parts of map MLOs where the props are baked into the map and cannot be targeted by model.
-- client.lua: Zone-based targeting
-- ox_target: Box zone for a reception desk
exports.ox_target:addBoxZone({
coords = vec3(441.8, -981.0, 30.7),
size = vec3(2.0, 1.0, 1.5),
rotation = 0,
debug = true, -- Shows the box in-game, set false for production
options = {
{
name = 'check_in',
icon = 'fas fa-clipboard-check',
label = 'Check In at Reception',
onSelect = function()
TriggerEvent('police:checkIn')
end,
canInteract = function()
return PlayerData.job == 'police'
end,
},
},
})
-- Sphere zone for a ground stash
local stashZone = exports.ox_target:addSphereZone({
coords = vec3(128.4, -1280.5, 29.0),
radius = 0.5,
debug = false,
options = {
{
name = 'open_stash',
icon = 'fas fa-box-open',
label = 'Open Stash',
distance = 1.5,
onSelect = function()
TriggerServerEvent('stash:open', 'ground_stash_01')
end,
},
},
})
-- Remove zone later if needed
exports.ox_target:removeZone(stashZone)
Conditional Visibility with canInteract
The canInteract function is what makes target systems truly powerful. It runs every time the crosshair hovers over a target and determines whether each option should be shown or hidden. This lets you create context-sensitive interactions where a police officer sees different options than a civilian, where a lockpick option only appears if the player has a lockpick item, or where a vehicle repair option only shows when the vehicle is damaged. The function receives the entity handle, distance, coordinates, and the option name as parameters. Keep the logic in canInteract lightweight because it runs on every frame while the player is hovering over the target. Avoid database queries or heavy calculations inside it. Instead, cache the required data in a local variable that gets updated through events.
-- client.lua: Advanced canInteract examples
-- Vehicle interaction with multiple conditional options
exports.ox_target:addGlobalVehicle({
{
name = 'repair_vehicle',
icon = 'fas fa-wrench',
label = 'Repair Vehicle',
distance = 3.0,
bones = {'engine'}, -- Only when targeting the engine area
canInteract = function(entity)
local health = GetVehicleEngineHealth(entity)
return health < 900.0 -- Only show if damaged
end,
onSelect = function(data)
TriggerEvent('mechanic:repair', data.entity)
end,
},
{
name = 'lockpick_vehicle',
icon = 'fas fa-key',
label = 'Lockpick Door',
distance = 2.0,
bones = {'door_dside_f', 'door_pside_f'},
canInteract = function(entity)
local locked = GetVehicleDoorLockStatus(entity)
local hasItem = exports.ox_inventory:Search('count', 'lockpick') > 0
return locked == 2 and hasItem
end,
onSelect = function(data)
TriggerEvent('lockpick:start', data.entity)
end,
},
{
name = 'open_trunk',
icon = 'fas fa-box',
label = 'Open Trunk',
distance = 2.5,
bones = {'boot'},
canInteract = function(entity)
return not IsVehicleDoorFullyOpen(entity, 5)
end,
onSelect = function(data)
SetVehicleDoorOpen(data.entity, 5, false, false)
TriggerEvent('inventory:openTrunk', data.entity)
end,
},
})
Global Targets: Player and Vehicle Interactions
Global targets apply to every player ped or every vehicle in the world without needing to register each one individually. addGlobalPlayer adds options that appear when targeting any other player, which is perfect for interactions like giving items, checking IDs, cuffing suspects, or healing other players. addGlobalVehicle adds options for every vehicle, useful for mechanics, police searches, or fueling systems. Use canInteract liberally with global targets to prevent option overload. A civilian should not see a cuff option, and a mechanic should only see repair options when on duty. Global targets are the most convenient way to add universal interactions but also the easiest to misuse, so think carefully about when each option should actually be visible.
-- client.lua: Global player and vehicle targets
-- Target any player for EMS interactions
exports.ox_target:addGlobalPlayer({
{
name = 'revive_player',
icon = 'fas fa-heartbeat',
label = 'Revive Player',
distance = 2.0,
canInteract = function(entity)
-- Only show if player is EMS and target is downed
local isEms = PlayerData.job == 'ambulance'
local targetState = Entity(entity).state.isDead
return isEms and targetState == true
end,
onSelect = function(data)
local targetServerId = GetPlayerServerId(NetworkGetPlayerIndexFromPed(data.entity))
TriggerServerEvent('ems:revive', targetServerId)
end,
},
{
name = 'give_item',
icon = 'fas fa-hand-holding',
label = 'Give Item',
distance = 2.0,
onSelect = function(data)
local targetServerId = GetPlayerServerId(NetworkGetPlayerIndexFromPed(data.entity))
TriggerEvent('inventory:giveItem', targetServerId)
end,
},
})
Performance Tips and Common Mistakes
Target systems are well-optimized but can still impact performance if misused. The most common mistake is registering too many zone targets with debug mode enabled in production. Debug rendering draws wireframe shapes for every zone, which is expensive when you have dozens of them. Always set debug = false before deploying. Another mistake is putting expensive logic inside canInteract functions. Since this runs on every frame while hovering, even a single exports.ox_inventory:Search call can add up if you have ten options all running inventory checks simultaneously. Cache these values instead and update them when the inventory changes. Finally, always clean up targets when your resource stops. Use the onResourceStop event handler to remove all zone, model, and entity targets. Leaked targets persist in memory and can cause errors or duplicate options if the resource is restarted. ox_target handles some cleanup automatically for local entities, but explicit cleanup is always the safest approach.