Radial Menu

The Radial Menu module provides sophisticated circular, gesture-based menu interfaces with dynamic positioning, multi-level navigation, and comprehensive interaction handling. Perfect for weapon wheels, quick actions, immersive gameplay interactions, and creating intuitive radial interfaces that enhance user experience with smooth animations and intelligent layout management.

showRadial

Display a radial menu with comprehensive configuration options and advanced positioning control.

-- Client-side
B2Lib.UI.showRadial(options)

-- Server-side (via exports)
exports.b2lib:showRadial(playerId, options)

Parameters:

  • playerId (number, server-side only): Player server ID to show radial menu to

  • options (table, required): Comprehensive radial menu configuration object

Configuration Options:

  • id (string, optional): Unique radial menu identifier for management and events (default: auto-generated UUID)

  • items (table, optional): Array of radial menu items with full configuration (default: empty array)

  • radius (number, optional): Base radius in pixels with responsive scaling (default: 100)

  • style (string, optional): Visual style preset ('default', 'minimal', 'modern', 'classic') (default: 'default')

  • showLabels (boolean, optional): Display item labels with positioning control (default: true)

  • showCenter (boolean, optional): Show center circle with interactive capabilities (default: true)

  • showConnectors (boolean, optional): Display lines connecting items to center with styling (default: false)

  • centerIcon (string, optional): Lucide icon to display in center circle

  • centerText (string, optional): Text to display in center with formatting support

  • centerSize (number, optional): Center circle size in pixels with scaling (default: 60)

  • itemSize (number, optional): Item circle size in pixels with responsive scaling (default: 50)

  • animationDuration (number, optional): Animation duration in milliseconds (default: 300)

  • expansionDelay (number, optional): Delay between item animations in milliseconds (default: 50)

  • selectionDeadzone (number, optional): Center deadzone radius in pixels (default: 35)

  • enableDynamicRadius (boolean, optional): Auto-adjust radius based on item count (default: false)

  • radiusPerItem (number, optional): Additional radius per item when dynamic (default: 8)

  • minRadius (number, optional): Minimum radius when dynamic scaling (default: 80)

  • maxRadius (number, optional): Maximum radius when dynamic scaling (default: 180)

  • position (table, optional): Custom positioning with anchor points

  • theme (string, optional): Theme override ('auto', 'light', 'dark')

  • zIndex (number, optional): Z-index for menu layering (default: 1000)

  • closeOnSelection (boolean, optional): Auto-close after item selection (default: true)

  • enableHapticFeedback (boolean, optional): Controller vibration feedback (default: true)

  • contextData (table, optional): Context data passed to event handlers

Returns: boolean - Success status indicating radial menu display completion

Example:

-- Basic radial menu
local success = B2Lib.UI.showRadial({
    items = {
        { label = 'Inventory', value = 'inventory', icon = 'package' },
        { label = 'Phone', value = 'phone', icon = 'smartphone' },
        { label = 'Vehicle', value = 'vehicle', icon = 'car' },
        { label = 'Settings', value = 'settings', icon = 'settings' }
    }
})

-- Advanced weapon wheel with comprehensive configuration
B2Lib.UI.showRadial({
    id = 'weapon_wheel',
    items = {
        { label = 'Pistol', value = 'weapon_pistol', icon = 'gun', badge = '250', badgeColor = '#10b981' },
        { label = 'Rifle', value = 'weapon_rifle', icon = 'rifle', badge = '120', badgeColor = '#f59e0b' },
        { label = 'Shotgun', value = 'weapon_shotgun', icon = 'shotgun', badge = '45', badgeColor = '#ef4444' },
        { label = 'Melee', value = 'weapon_melee', icon = 'sword', disabled = false }
    },
    radius = 120,
    centerIcon = 'crosshair',
    centerText = 'Weapons',
    showConnectors = true,
    animationDuration = 250,
    expansionDelay = 30,
    style = 'modern',
    enableHapticFeedback = true,
    contextData = {
        playerId = PlayerId(),
        currentWeapon = GetSelectedPedWeapon(PlayerPedId())
    }
})

-- Dynamic radius radial menu with conditional items
B2Lib.UI.showRadial({
    items = getPlayerActions(), -- Custom function returning many items
    enableDynamicRadius = true,
    radiusPerItem = 10,
    minRadius = 100,
    maxRadius = 200,
    showLabels = true,
    centerText = 'Actions',
    position = { x = 960, y = 540, anchor = 'center' },
    theme = 'auto'
})

-- Server-side radial menu for admin tools
exports.b2lib:showRadial(playerId, {
    id = 'admin_tools',
    items = {
        { label = 'Player Management', value = 'players', icon = 'users', badge = tostring(#GetPlayers()) },
        { label = 'Vehicle Spawn', value = 'vehicles', icon = 'car' },
        { label = 'Teleport Tools', value = 'teleport', icon = 'zap' },
        { label = 'Weather Control', value = 'weather', icon = 'cloud' }
    },
    centerIcon = 'shield-check',
    centerText = 'Admin',
    radius = 130,
    showConnectors = true
})

hideRadial

Hide the currently active radial menu with smooth animation and proper cleanup.

-- Client-side
B2Lib.UI.hideRadial()

-- Server-side (via exports)
exports.b2lib:hideRadial(playerId)

Parameters:

  • playerId (number, server-side only): Player server ID to hide radial menu for

Returns: boolean - Success status indicating radial menu hide completion

Example:

-- Hide the current radial menu
local success = B2Lib.UI.hideRadial()

-- Hide radial menu when player moves
CreateThread(function()
    while true do
        Wait(100)
        
        if IsControlPressed(0, 21) then -- Sprint
            B2Lib.UI.hideRadial()
        end
    end
end)

-- Server-side hide for specific player
exports.b2lib:hideRadial(playerId)

updateRadialData

Update context data for the currently active radial menu with real-time data refresh.

B2Lib.UI.updateRadialData(contextData)

Parameters:

  • contextData (table, required): Updated context data object with new values

Returns: boolean - Success status indicating data update completion

Example:

-- Update radial context data dynamically
local success = B2Lib.UI.updateRadialData({
    currentWeapon = GetSelectedPedWeapon(PlayerPedId()),
    ammoCount = GetAmmoInPedWeapon(PlayerPedId(), GetSelectedPedWeapon(PlayerPedId())),
    playerHealth = GetEntityHealth(PlayerPedId())
})

-- Update vehicle radial data
B2Lib.UI.updateRadialData({
    vehicle = GetVehiclePedIsIn(PlayerPedId(), false),
    engineHealth = GetVehicleEngineHealth(vehicle),
    fuelLevel = GetVehicleFuelLevel(vehicle)
})

isRadialVisible

Check if a radial menu is currently visible and get active menu information.

B2Lib.UI.isRadialVisible()

Returns: boolean|table - False if no radial menu is active, or table with active menu data

  • id (string): Current radial menu identifier

  • itemCount (number): Number of menu items

  • radius (number): Current menu radius

  • contextData (table): Current context data

Example:

local radialState = B2Lib.UI.isRadialVisible()
if radialState then
    print('Radial menu is visible:', radialState.id)
    print('Item count:', radialState.itemCount)
    print('Radius:', radialState.radius)
else
    print('No radial menu is currently visible')
end

updateRadialItems

Update a radial menu's items dynamically while maintaining position and state.

B2Lib.UI.updateRadialItems(items)

Parameters:

  • items (table, required): Updated array of radial menu items

Returns: boolean - Success status indicating items update completion

Example:

-- Update weapon wheel based on current weapons
local weapons = getCurrentWeapons() -- Custom function
B2Lib.UI.updateRadialItems(weapons)

-- Update admin tools based on permissions
local adminItems = getAdminItems(getPlayerAdminLevel()) -- Custom function
B2Lib.UI.updateRadialItems(adminItems)

Radial Menu Item Structure

Radial menu items support comprehensive properties for advanced functionality and visual customization:

local radialItem = {
    label = 'Item Label',                    -- Display text (required)
    value = 'item_value',                    -- Item value/ID for identification (required)
    icon = 'icon_name',                      -- Lucide icon name (required)
    description = 'Item description',        -- Optional tooltip description text
    disabled = false,                        -- Whether item is disabled/non-interactive
    color = '#ffffff',                       -- Custom item text color
    backgroundColor = '#333333',             -- Custom item background color
    borderColor = '#666666',                 -- Custom item border color
    badge = '5',                             -- Optional badge text/number for notifications
    badgeColor = '#ff0000',                  -- Badge background color
    badgeTextColor = '#ffffff',              -- Badge text color
    shortcut = 'F1',                         -- Optional keyboard shortcut display
    progress = 75,                           -- Optional progress indicator (0-100)
    progressColor = '#10b981',               -- Progress bar color
    condition = function(contextData)        -- Optional condition function for dynamic visibility
        return contextData.hasPermission
    end,
    onClick = function(contextData)          -- Optional custom click handler
        print('Custom action executed')
    end,
    submenu = {                              -- Optional submenu items for nested navigation
        { label = 'Sub Item 1', value = 'sub1', icon = 'circle' },
        { label = 'Sub Item 2', value = 'sub2', icon = 'square' }
    },
    style = {                                -- Optional custom styling
        fontSize = '14px',
        fontWeight = 'bold',
        iconSize = '20px',
        borderRadius = '50%'
    },
    animation = {                            -- Optional custom animation settings
        duration = 300,
        delay = 0,
        easing = 'ease-out'
    }
}

Server-Side Usage

The Radial Menu module provides comprehensive server-side functionality for managing player-specific radial menus and administrative tools:

-- Show radial menu to specific player
exports.b2lib:showRadial(playerId, {
    id = 'admin_tools',
    items = {
        { label = 'Player Management', value = 'players', icon = 'users', badge = tostring(#GetPlayers()) },
        { label = 'Vehicle Spawn', value = 'vehicles', icon = 'car' },
        { label = 'Teleport Tools', value = 'teleport', icon = 'zap' },
        { label = 'Weather Control', value = 'weather', icon = 'cloud' },
        { label = 'Time Control', value = 'time', icon = 'clock' }
    },
    centerIcon = 'shield-check',
    centerText = 'Admin Tools',
    radius = 140,
    showConnectors = true,
    contextData = {
        adminLevel = getPlayerAdminLevel(playerId),
        playerCount = #GetPlayers(),
        serverTime = os.time()
    }
})

-- Hide radial menu for specific player
exports.b2lib:hideRadial(playerId)

-- Show radial menu to multiple players
local function showRadialToPlayers(playerIds, options)
    for _, playerId in ipairs(playerIds) do
        exports.b2lib:showRadial(playerId, options)
    end
end

-- Show radial menu to all admins
local function showAdminRadial(options)
    local players = GetPlayers()
    for _, playerId in ipairs(players) do
        if IsPlayerAceAllowed(playerId, 'admin') then
            exports.b2lib:showRadial(tonumber(playerId), options)
        end
    end
end

-- Example: Dynamic admin radial based on permissions
RegisterNetEvent('admin:showRadialMenu')
AddEventHandler('admin:showRadialMenu', function()
    local source = source
    local adminLevel = getPlayerAdminLevel(source)
    
    if adminLevel < 1 then
        TriggerClientEvent('b2lib:notify', source, {
            type = 'error',
            message = 'Insufficient permissions'
        })
        return
    end
    
    local items = {}
    
    -- Basic admin tools
    if adminLevel >= 1 then
        table.insert(items, { label = 'Player List', value = 'players', icon = 'users' })
        table.insert(items, { label = 'Kick Player', value = 'kick', icon = 'user-x', color = '#f59e0b' })
    end
    
    -- Advanced admin tools
    if adminLevel >= 2 then
        table.insert(items, { label = 'Ban Player', value = 'ban', icon = 'shield-off', color = '#ef4444' })
        table.insert(items, { label = 'Vehicle Spawn', value = 'vehicles', icon = 'car' })
        table.insert(items, { label = 'Teleport', value = 'teleport', icon = 'zap' })
    end
    
    -- Super admin tools
    if adminLevel >= 3 then
        table.insert(items, { label = 'Weather', value = 'weather', icon = 'cloud' })
        table.insert(items, { label = 'Time', value = 'time', icon = 'clock' })
        table.insert(items, { label = 'Server Control', value = 'server', icon = 'server', color = '#ef4444' })
    end
    
    exports.b2lib:showRadial(source, {
        id = 'admin_radial',
        items = items,
        centerIcon = 'shield-check',
        centerText = 'Admin Level ' .. adminLevel,
        radius = 120 + (#items * 5), -- Dynamic radius based on item count
        enableDynamicRadius = true,
        contextData = {
            adminLevel = adminLevel,
            playerId = source,
            timestamp = os.time()
        }
    })
end)

-- Example: Zone-based radial menus
local function showZoneRadial(coords, radius, radialOptions)
    local players = GetPlayers()
    
    for _, playerId in ipairs(players) do
        local playerPed = GetPlayerPed(playerId)
        if playerPed and DoesEntityExist(playerPed) then
            local playerCoords = GetEntityCoords(playerPed)
            local distance = #(playerCoords - coords)
            
            if distance <= radius then
                exports.b2lib:showRadial(tonumber(playerId), radialOptions)
            end
        end
    end
end

-- Example: Event-based radial management
RegisterNetEvent('server:showVehicleRadial')
AddEventHandler('server:showVehicleRadial', function(vehicleNetId)
    local source = source
    local vehicle = NetworkGetEntityFromNetworkId(vehicleNetId)
    
    if DoesEntityExist(vehicle) then
        local model = GetEntityModel(vehicle)
        local displayName = GetDisplayNameFromVehicleModel(model)
        local plate = GetVehicleNumberPlateText(vehicle)
        
        exports.b2lib:showRadial(source, {
            id = 'vehicle_radial',
            items = {
                { label = 'Enter Vehicle', value = 'enter', icon = 'car' },
                { label = 'Lock/Unlock', value = 'lock', icon = 'lock' },
                { label = 'Open Hood', value = 'hood', icon = 'wrench' },
                { label = 'Open Trunk', value = 'trunk', icon = 'package' },
                { label = 'Vehicle Info', value = 'info', icon = 'info' }
            },
            centerIcon = 'car',
            centerText = displayName,
            radius = 110,
            contextData = {
                vehicle = vehicle,
                model = model,
                plate = plate,
                playerId = source
            }
        })
    end
end)

-- Example: Timed radial menus
local function showTimedRadial(playerId, options, duration)
    exports.b2lib:showRadial(playerId, options)
    
    SetTimeout(duration, function()
        exports.b2lib:hideRadial(playerId)
    end)
end

-- Example: Permission-based radial items
local function getPermissionBasedItems(playerId)
    local items = {}
    
    -- Basic items for all players
    table.insert(items, { label = 'Inventory', value = 'inventory', icon = 'package' })
    table.insert(items, { label = 'Phone', value = 'phone', icon = 'smartphone' })
    
    -- VIP items
    if IsPlayerAceAllowed(playerId, 'vip') then
        table.insert(items, { label = 'VIP Lounge', value = 'vip', icon = 'crown', color = '#fbbf24' })
    end
    
    -- Police items
    if IsPlayerAceAllowed(playerId, 'police') then
        table.insert(items, { label = 'Police Radio', value = 'radio', icon = 'radio', color = '#3b82f6' })
        table.insert(items, { label = 'Arrest', value = 'arrest', icon = 'handcuffs', color = '#ef4444' })
    end
    
    -- Medic items
    if IsPlayerAceAllowed(playerId, 'medic') then
        table.insert(items, { label = 'Medical Kit', value = 'medkit', icon = 'heart-pulse', color = '#10b981' })
        table.insert(items, { label = 'Revive', value = 'revive', icon = 'heart', color = '#f59e0b' })
    end
    
    return items
end

Events

The Radial Menu module provides comprehensive event handling for monitoring and responding to radial menu interactions:

Client Events

-- Radial menu item selected event
AddEventHandler('b2lib:radial-item-selected', function(data)
    print('Radial item selected:', data.value)
    print('Radial ID:', data.id)
    print('Item data:', json.encode(data.item))
    print('Context data:', json.encode(data.contextData))
    print('Selection angle:', data.angle)
    print('Selection time:', data.selectionTime)
    
    -- Handle different radial menu types
    if data.id == 'weapon_wheel' then
        if data.value == 'weapon_pistol' then
            GiveWeaponToPed(PlayerPedId(), GetHashKey('WEAPON_PISTOL'), 250, false, true)
            B2Lib.UI.notify({ type = 'success', message = 'Equipped Pistol' })
        elseif data.value == 'weapon_rifle' then
            GiveWeaponToPed(PlayerPedId(), GetHashKey('WEAPON_ASSAULTRIFLE'), 250, false, true)
            B2Lib.UI.notify({ type = 'success', message = 'Equipped Assault Rifle' })
        end
    elseif data.value == 'inventory' then
        TriggerEvent('inventory:open')
    elseif data.value == 'phone' then
        TriggerEvent('phone:open')
    elseif data.value == 'vehicle' then
        showVehicleRadialMenu()
    end
end)

-- Radial menu opened event
AddEventHandler('b2lib:radial-opened', function(data)
    print('Radial menu opened:', data.id)
    print('Item count:', data.itemCount)
    print('Radius:', data.radius)
    print('Center position:', json.encode(data.centerPosition))
    
    -- Disable certain controls while radial is open
    CreateThread(function()
        while B2Lib.UI.isRadialVisible() do
            Wait(0)
            DisableControlAction(0, 1, true)   -- LookLeftRight
            DisableControlAction(0, 2, true)   -- LookUpDown
            DisableControlAction(0, 24, true)  -- Attack
            DisableControlAction(0, 25, true)  -- Aim
            DisableControlAction(0, 142, true) -- MeleeAttackAlternate
            DisableControlAction(0, 106, true) -- VehicleMouseControlOverride
        end
    end)
    
    -- Show help text
    if data.id == 'weapon_wheel' then
        B2Lib.UI.textUI({
            text = 'Move mouse to select weapon, click to equip',
            position = 'bottom'
        })
    end
end)

-- Radial menu closed event
AddEventHandler('b2lib:radial-closed', function(data)
    print('Radial menu closed:', data.id)
    print('Close reason:', data.reason) -- 'selection', 'escape', 'outside-click', 'manual'
    print('Duration open:', data.duration)
    
    -- Re-enable controls
    EnableAllControlActions(0)
    
    -- Hide help text
    B2Lib.UI.hideTextUI()
    
    -- Clean up radial-specific resources
    if data.id == 'weapon_wheel' then
        -- Clean up weapon wheel state
        TriggerEvent('weapons:cleanupWheel')
    elseif data.id == 'vehicle_radial' then
        -- Clean up vehicle interaction state
        TriggerEvent('vehicle:cleanupRadial')
    end
end)

-- Radial menu item hovered event
AddEventHandler('b2lib:radial-item-hovered', function(data)
    print('Item hovered:', data.value)
    print('Item label:', data.item.label)
    print('Hover angle:', data.angle)
    print('Distance from center:', data.distance)
    
    -- Show item description in UI
    if data.item.description then
        B2Lib.UI.textUI({
            text = data.item.description,
            position = 'bottom'
        })
    end
    
    -- Update center text with item info
    if data.item.badge then
        B2Lib.UI.updateRadialCenter({
            text = data.item.label .. ' (' .. data.item.badge .. ')',
            icon = data.item.icon
        })
    end
    
    -- Haptic feedback for controllers
    if data.contextData and data.contextData.enableHapticFeedback then
        TriggerEvent('gamepad:vibrate', 100, 0.1)
    end
end)

-- Radial menu item unhovered event
AddEventHandler('b2lib:radial-item-unhovered', function(data)
    print('Item unhovered:', data.value)
    
    -- Reset center text
    B2Lib.UI.updateRadialCenter({
        text = data.originalCenterText,
        icon = data.originalCenterIcon
    })
    
    -- Hide item description
    B2Lib.UI.hideTextUI()
end)

-- Radial menu updated event
AddEventHandler('b2lib:radial-updated', function(data)
    print('Radial menu updated:', data.id)
    print('Update type:', data.updateType) -- 'items', 'data', 'style'
    print('Changes:', json.encode(data.changes))
end)

Server Events

-- Player radial menu shown event
AddEventHandler('b2lib:radial-player-shown', function(playerId, radialId, contextData)
    print('Radial menu shown to player', playerId, ':', radialId)
    
    -- Log admin radial usage
    if radialId:find('admin') then
        print('Admin radial menu shown to player', playerId)
        -- Log to admin activity system
        logAdminActivity(playerId, 'radial_menu_opened', radialId)
    end
    
    -- Track radial menu usage
    trackRadialUsage(playerId, radialId, 'opened')
end)

-- Player radial menu item selected event
AddEventHandler('b2lib:radial-player-selected', function(playerId, radialId, itemValue, contextData)
    print('Player', playerId, 'selected item:', itemValue, 'from radial:', radialId)
    
    -- Handle server-side radial actions
    if radialId == 'admin_radial' then
        handleAdminRadialAction(playerId, itemValue, contextData)
    elseif radialId == 'vehicle_radial' then
        handleVehicleRadialAction(playerId, itemValue, contextData)
    end
    
    -- Track item selection
    trackRadialUsage(playerId, radialId, 'item_selected', itemValue)
end)

-- Player radial menu closed event
AddEventHandler('b2lib:radial-player-closed', function(playerId, radialId, reason)
    print('Radial menu closed for player', playerId, ':', radialId, 'reason:', reason)
    
    -- Track radial menu usage
    trackRadialUsage(playerId, radialId, 'closed', reason)
end)

Configuration

Complete Radial Menu configuration options in config.lua:

--- Radial Menu configuration
--- @type table
Config.RadialMenu = {
    defaultRadius = 100,                -- Default radius in pixels with responsive scaling
    defaultItemSize = 50,               -- Default item size in pixels
    defaultCenterSize = 60,             -- Default center size in pixels with scaling
    animationDuration = 300,            -- Default animation duration in milliseconds
    expansionDelay = 50,                -- Default expansion delay between items in milliseconds
    selectionDeadzone = 35,             -- Default selection deadzone radius in pixels
    showLabels = true,                  -- Show item labels by default
    showCenter = true,                  -- Show center circle by default
    showConnectors = false,             -- Show connector lines by default
    enableDynamicRadius = false,        -- Enable dynamic radius adjustment by default
    closeOnSelection = true,            -- Close menu after item selection
    enableHapticFeedback = true,        -- Enable controller vibration feedback
    maxItems = 12,                      -- Maximum number of items in a single radial
    minRadius = 80,                     -- Minimum radius when using dynamic sizing
    maxRadius = 200,                    -- Maximum radius when using dynamic sizing
    radiusPerItem = 8,                  -- Additional radius per item when dynamic
    labelDistance = 25,                 -- Distance of labels from items in pixels
    connectorOpacity = 0.3,             -- Opacity of connector lines (0-1)
    selectionSensitivity = 0.8,         -- Mouse sensitivity for selection (0-1)
    keyboardNavigation = true,          -- Enable keyboard navigation support
    touchSupport = true,                -- Enable touch/mobile support
    debugMode = false                   -- Enable debug visualization
}

-- Radial menu styling configuration
Config.ComponentStyles.radialMenu = {
    -- Container styling
    container = {
        zIndex = '1000',                -- Z-index for layering and modal behavior
        userSelect = 'none',            -- Disable text selection for better UX
        pointerEvents = 'auto',         -- Enable pointer events for interaction
        position = 'fixed',             -- Fixed positioning for screen overlay
        top = '0',                      -- Full screen coverage
        left = '0',
        width = '100vw',
        height = '100vh',
        display = 'flex',
        alignItems = 'center',
        justifyContent = 'center'
    },
    
    -- Center circle styling
    center = {
        borderRadius = '50%',           -- Perfect circular shape
        borderWidth = '2px',            -- Border thickness
        borderStyle = 'solid',          -- Solid border style
        fontSize = 'sm',                -- Text font size
        fontWeight = 'medium',          -- Text font weight
        transition = 'all 0.2s ease',  -- Smooth transitions for all properties
        cursor = 'default',             -- Default cursor style
        display = 'flex',               -- Flexbox for centering content
        alignItems = 'center',          -- Vertical centering
        justifyContent = 'center',      -- Horizontal centering
        flexDirection = 'column',       -- Stack icon and text vertically
        textAlign = 'center',           -- Center text alignment
        boxShadow = '0 4px 12px rgba(0,0,0,0.15)', -- Subtle shadow
        backdropFilter = 'blur(4px)'    -- Background blur effect
    },
    
    -- Item styling
    item = {
        borderRadius = '50%',           -- Perfect circular shape
        borderWidth = '2px',            -- Border thickness
        borderStyle = 'solid',          -- Solid border style
        fontSize = 'xs',                -- Label font size
        fontWeight = 'medium',          -- Label font weight
        transition = 'all 0.2s ease',  -- Smooth transitions for all properties
        cursor = 'pointer',             -- Pointer cursor for interactivity
        iconSize = '20px',              -- Icon size within items
        labelOffset = '8px',            -- Label distance from item circle
        display = 'flex',               -- Flexbox for centering content
        alignItems = 'center',          -- Vertical centering
        justifyContent = 'center',      -- Horizontal centering
        position = 'absolute',          -- Absolute positioning for circular layout
        boxShadow = '0 2px 8px rgba(0,0,0,0.1)', -- Subtle shadow
        backdropFilter = 'blur(2px)',   -- Background blur effect
        transform = 'scale(1)',         -- Default scale for animations
        transformOrigin = 'center'      -- Transform origin for scaling
    },
    
    -- Item hover styling
    itemHover = {
        transform = 'scale(1.1)',       -- Scale up on hover
        boxShadow = '0 4px 16px rgba(0,0,0,0.2)', -- Enhanced shadow on hover
        zIndex = '10'                   -- Bring hovered item to front
    },
    
    -- Item disabled styling
    itemDisabled = {
        opacity = '0.5',                -- Reduced opacity for disabled items
        cursor = 'not-allowed',         -- Not-allowed cursor
        transform = 'scale(0.9)'        -- Slightly smaller scale
    },
    
    -- Label styling
    label = {
        position = 'absolute',          -- Absolute positioning relative to item
        fontSize = 'xs',                -- Small font size
        fontWeight = 'medium',          -- Medium font weight
        textAlign = 'center',           -- Center text alignment
        whiteSpace = 'nowrap',          -- Prevent text wrapping
        pointerEvents = 'none',         -- Disable pointer events on labels
        textShadow = '0 1px 2px rgba(0,0,0,0.5)', -- Text shadow for readability
        transition = 'all 0.2s ease'   -- Smooth transitions
    },
    
    -- Connector line styling
    connector = {
        strokeWidth = '2px',            -- Line thickness
        opacity = '0.3',                -- Line opacity for subtle appearance
        transition = 'all 0.2s ease',  -- Smooth transitions
        strokeDasharray = 'none',       -- Solid line by default
        strokeLinecap = 'round'         -- Rounded line caps
    },
    
    -- Badge styling
    badge = {
        borderRadius = '50%',           -- Circular badge shape
        fontSize = 'xs',                -- Small badge font size
        fontWeight = 'bold',            -- Bold badge text
        minWidth = '18px',              -- Minimum badge width
        height = '18px',                -- Badge height
        top = '-6px',                   -- Badge position from top
        right = '-6px',                 -- Badge position from right
        position = 'absolute',          -- Absolute positioning
        display = 'flex',               -- Flexbox for centering
        alignItems = 'center',          -- Vertical centering
        justifyContent = 'center',      -- Horizontal centering
        border = '2px solid white',     -- White border for contrast
        boxShadow = '0 1px 3px rgba(0,0,0,0.3)' -- Shadow for depth
    },
    
    -- Progress indicator styling
    progress = {
        position = 'absolute',          -- Absolute positioning
        top = '0',                      -- Full coverage of item
        left = '0',
        width = '100%',
        height = '100%',
        borderRadius = '50%',           -- Circular progress
        background = 'conic-gradient(from 0deg, transparent 0deg, var(--progress-color) 0deg, transparent 0deg)', -- Conic gradient for progress
        opacity = '0.7',                -- Semi-transparent
        pointerEvents = 'none'          -- Don't interfere with interactions
    },
    
    -- Animation presets
    animations = {
        fadeIn = 'fadeIn 0.3s ease-out',
        fadeOut = 'fadeOut 0.2s ease-in',
        scaleIn = 'scaleIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)',
        scaleOut = 'scaleOut 0.2s ease-in',
        slideIn = 'slideIn 0.3s ease-out',
        slideOut = 'slideOut 0.2s ease-in',
        rotateIn = 'rotateIn 0.3s ease-out',
        bounceIn = 'bounceIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55)'
    },
    
    -- Color themes
    themes = {
        default = {
            centerBackground = 'rgba(255, 255, 255, 0.95)',
            centerBorder = 'rgba(0, 0, 0, 0.1)',
            centerText = '#374151',
            itemBackground = 'rgba(255, 255, 255, 0.9)',
            itemBorder = 'rgba(0, 0, 0, 0.1)',
            itemText = '#374151',
            connectorColor = 'rgba(0, 0, 0, 0.2)',
            labelText = '#374151'
        },
        dark = {
            centerBackground = 'rgba(31, 41, 55, 0.95)',
            centerBorder = 'rgba(255, 255, 255, 0.1)',
            centerText = '#f3f4f6',
            itemBackground = 'rgba(31, 41, 55, 0.9)',
            itemBorder = 'rgba(255, 255, 255, 0.1)',
            itemText = '#f3f4f6',
            connectorColor = 'rgba(255, 255, 255, 0.2)',
            labelText = '#f3f4f6'
        }
    }
}

Advanced Examples

Weapon Wheel System

-- Create a comprehensive weapon wheel
local function createWeaponWheel()
    local playerPed = PlayerPedId()
    local weapons = {}
    
    -- Get player's weapons
    for _, weaponHash in ipairs(Config.Weapons) do
        if HasPedGotWeapon(playerPed, weaponHash, false) then
            local weaponData = Config.WeaponData[weaponHash]
            table.insert(weapons, {
                label = weaponData.label,
                value = weaponData.name,
                icon = weaponData.icon,
                hash = weaponHash,
                ammo = GetAmmoInPedWeapon(playerPed, weaponHash)
            })
        end
    end
    
    -- Add unarmed option
    table.insert(weapons, 1, {
        label = 'Unarmed',
        value = 'unarmed',
        icon = 'hand',
        hash = GetHashKey('WEAPON_UNARMED')
    })
    
    B2Lib.UI.showRadial({
        id = 'weapon_wheel',
        items = weapons,
        radius = 120,
        centerIcon = 'crosshair',
        centerText = 'Weapons',
        showConnectors = true,
        animationDuration = 200,
        expansionDelay = 25,
        enableDynamicRadius = true,
        minRadius = 100,
        maxRadius = 160
    })
    
    -- Handle weapon selection
    AddEventHandler('b2lib:radial-item-selected', function(data)
        if data.radialId == 'weapon_wheel' then
            local weaponHash = data.item.hash
            SetCurrentPedWeapon(playerPed, weaponHash, true)
            
            B2Lib.UI.notify({
                type = 'info',
                message = 'Equipped: ' .. data.item.label
            })
        end
    end)
end

-- Bind to key
RegisterKeyMapping('weaponwheel', 'Open Weapon Wheel', 'keyboard', 'TAB')
RegisterCommand('weaponwheel', function()
    createWeaponWheel()
end)

Vehicle Interaction Radial

-- Create a vehicle interaction radial menu
local function createVehicleRadial()
    local playerPed = PlayerPedId()
    local vehicle = GetVehiclePedIsIn(playerPed, false)
    
    if vehicle == 0 then
        vehicle = GetClosestVehicle(GetEntityCoords(playerPed), 5.0, 0, 71)
        if vehicle == 0 then
            B2Lib.UI.notify({
                type = 'error',
                message = 'No vehicle nearby'
            })
            return
        end
    end
    
    local isInVehicle = GetVehiclePedIsIn(playerPed, false) ~= 0
    local isLocked = GetVehicleDoorLockStatus(vehicle) == 2
    local engineOn = GetIsVehicleEngineRunning(vehicle)
    
    local items = {}
    
    if isInVehicle then
        -- Inside vehicle options
        table.insert(items, {
            label = engineOn and 'Turn Off Engine' or 'Turn On Engine',
            value = 'toggle_engine',
            icon = engineOn and 'engine-off' or 'engine',
            color = engineOn and '#ff6b6b' or '#51cf66'
        })
        
        table.insert(items, {
            label = 'Toggle Lights',
            value = 'toggle_lights',
            icon = 'lightbulb'
        })
        
        table.insert(items, {
            label = 'Open Hood',
            value = 'open_hood',
            icon = 'wrench'
        })
        
        table.insert(items, {
            label = 'Open Trunk',
            value = 'open_trunk',
            icon = 'package'
        })
        
        table.insert(items, {
            label = 'Exit Vehicle',
            value = 'exit_vehicle',
            icon = 'exit'
        })
    else
        -- Outside vehicle options
        table.insert(items, {
            label = 'Enter Vehicle',
            value = 'enter_vehicle',
            icon = 'car',
            disabled = isLocked
        })
        
        table.insert(items, {
            label = isLocked and 'Unlock' or 'Lock',
            value = 'toggle_lock',
            icon = isLocked and 'unlock' or 'lock'
        })
        
        table.insert(items, {
            label = 'Inspect',
            value = 'inspect',
            icon = 'search'
        })
        
        if not isLocked then
            table.insert(items, {
                label = 'Open Hood',
                value = 'open_hood',
                icon = 'wrench'
            })
            
            table.insert(items, {
                label = 'Open Trunk',
                value = 'open_trunk',
                icon = 'package'
            })
        end
    end
    
    B2Lib.UI.showRadial({
        id = 'vehicle_radial',
        items = items,
        centerIcon = 'car',
        centerText = GetDisplayNameFromVehicleModel(GetEntityModel(vehicle)),
        radius = 110,
        showConnectors = false
    })
    
    -- Handle vehicle actions
    AddEventHandler('b2lib:radial-item-selected', function(data)
        if data.radialId == 'vehicle_radial' then
            if data.value == 'toggle_engine' then
                SetVehicleEngineOn(vehicle, not engineOn, false, true)
            elseif data.value == 'toggle_lights' then
                local lightsOn = IsVehicleLightOn(vehicle)
                SetVehicleLights(vehicle, lightsOn and 1 or 2)
            elseif data.value == 'open_hood' then
                SetVehicleDoorOpen(vehicle, 4, false, false)
            elseif data.value == 'open_trunk' then
                SetVehicleDoorOpen(vehicle, 5, false, false)
            elseif data.value == 'enter_vehicle' then
                TaskEnterVehicle(playerPed, vehicle, -1, -1, 1.0, 1, 0)
            elseif data.value == 'exit_vehicle' then
                TaskLeaveVehicle(playerPed, vehicle, 0)
            elseif data.value == 'toggle_lock' then
                SetVehicleDoorsLocked(vehicle, isLocked and 1 or 2)
            elseif data.value == 'inspect' then
                -- Show vehicle info
                local health = GetVehicleEngineHealth(vehicle)
                local fuel = GetVehicleFuelLevel(vehicle)
                B2Lib.UI.notify({
                    type = 'info',
                    title = 'Vehicle Info',
                    message = string.format('Health: %.0f%% | Fuel: %.0f%%', health/10, fuel)
                })
            end
        end
    end)
end

-- Bind to key
RegisterKeyMapping('vehicleradial', 'Vehicle Radial Menu', 'keyboard', 'Y')
RegisterCommand('vehicleradial', function()
    createVehicleRadial()
end)

Emote Radial Menu

-- Create an emote radial menu
local function createEmoteRadial()
    local emoteCategories = {
        {
            label = 'Greetings',
            value = 'greetings',
            icon = 'hand-wave',
            submenu = {
                { label = 'Wave', value = 'wave', icon = 'hand' },
                { label = 'Salute', value = 'salute', icon = 'salute' },
                { label = 'Handshake', value = 'handshake', icon = 'handshake' },
                { label = 'Hug', value = 'hug', icon = 'heart' }
            }
        },
        {
            label = 'Dance',
            value = 'dance',
            icon = 'music',
            submenu = {
                { label = 'Dance 1', value = 'dance1', icon = 'music' },
                { label = 'Dance 2', value = 'dance2', icon = 'music' },
                { label = 'Dance 3', value = 'dance3', icon = 'music' }
            }
        },
        {
            label = 'Gestures',
            value = 'gestures',
            icon = 'hand-point',
            submenu = {
                { label = 'Point', value = 'point', icon = 'hand-point' },
                { label = 'Thumbs Up', value = 'thumbsup', icon = 'thumbs-up' },
                { label = 'Thumbs Down', value = 'thumbsdown', icon = 'thumbs-down' },
                { label = 'Facepalm', value = 'facepalm', icon = 'face-palm' }
            }
        },
        {
            label = 'Poses',
            value = 'poses',
            icon = 'user',
            submenu = {
                { label = 'Lean Wall', value = 'leanwall', icon = 'wall' },
                { label = 'Sit', value = 'sit', icon = 'chair' },
                { label = 'Lay Down', value = 'laydown', icon = 'bed' }
            }
        }
    }
    
    B2Lib.UI.showRadial({
        id = 'emote_radial',
        items = emoteCategories,
        centerIcon = 'smile',
        centerText = 'Emotes',
        radius = 100,
        showLabels = true
    })
    
    -- Handle emote selection
    AddEventHandler('b2lib:radial-item-selected', function(data)
        if data.radialId == 'emote_radial' then
            if data.item.submenu then
                -- Show submenu
                B2Lib.UI.showRadial({
                    id = 'emote_submenu',
                    items = data.item.submenu,
                    centerIcon = data.item.icon,
                    centerText = data.item.label,
                    radius = 90
                })
            end
        elseif data.radialId == 'emote_submenu' then
            -- Play emote
            local emoteName = data.value
            TriggerEvent('emotes:play', emoteName)
            
            B2Lib.UI.notify({
                type = 'success',
                message = 'Playing emote: ' .. data.item.label
            })
        end
    end)
end

-- Bind to key
RegisterKeyMapping('emoteradial', 'Emote Radial Menu', 'keyboard', 'F3')
RegisterCommand('emoteradial', function()
    createEmoteRadial()
end)

Admin Tools Radial

-- Create an admin tools radial menu
local function createAdminRadial()
    -- Check admin permissions
    if not isPlayerAdmin() then -- Custom function
        B2Lib.UI.notify({
            type = 'error',
            message = 'Insufficient permissions'
        })
        return
    end
    
    local adminTools = {
        {
            label = 'Player Management',
            value = 'players',
            icon = 'users',
            badge = tostring(#GetActivePlayers()),
            badgeColor = '#3b82f6'
        },
        {
            label = 'Vehicle Spawn',
            value = 'vehicles',
            icon = 'car',
            color = '#10b981'
        },
        {
            label = 'Teleport',
            value = 'teleport',
            icon = 'zap',
            color = '#f59e0b'
        },
        {
            label = 'Weather',
            value = 'weather',
            icon = 'cloud',
            color = '#06b6d4'
        },
        {
            label = 'Time',
            value = 'time',
            icon = 'clock',
            color = '#8b5cf6'
        },
        {
            label = 'Noclip',
            value = 'noclip',
            icon = 'ghost',
            color = '#ef4444'
        },
        {
            label = 'Invisible',
            value = 'invisible',
            icon = 'eye-off',
            color = '#6b7280'
        },
        {
            label = 'God Mode',
            value = 'godmode',
            icon = 'shield',
            color = '#fbbf24'
        }
    }
    
    B2Lib.UI.showRadial({
        id = 'admin_radial',
        items = adminTools,
        centerIcon = 'shield-check',
        centerText = 'Admin',
        radius = 130,
        enableDynamicRadius = true,
        showConnectors = true,
        animationDuration = 250
    })
    
    -- Handle admin tool selection
    AddEventHandler('b2lib:radial-item-selected', function(data)
        if data.radialId == 'admin_radial' then
            if data.value == 'players' then
                -- Show player management menu
                showPlayerManagementMenu()
            elseif data.value == 'vehicles' then
                -- Show vehicle spawn menu
                showVehicleSpawnMenu()
            elseif data.value == 'teleport' then
                -- Show teleport options
                showTeleportMenu()
            elseif data.value == 'weather' then
                -- Show weather control
                showWeatherMenu()
            elseif data.value == 'time' then
                -- Show time control
                showTimeMenu()
            elseif data.value == 'noclip' then
                -- Toggle noclip
                toggleNoclip()
            elseif data.value == 'invisible' then
                -- Toggle invisibility
                toggleInvisibility()
            elseif data.value == 'godmode' then
                -- Toggle god mode
                toggleGodMode()
            end
        end
    end)
end

-- Bind to key (admin only)
RegisterKeyMapping('adminradial', 'Admin Radial Menu', 'keyboard', 'F6')
RegisterCommand('adminradial', function()
    createAdminRadial()
end)

Best Practices

  1. Optimize Item Count - Keep radial menus to 8-12 items for optimal usability and visual clarity

  2. Use Meaningful Icons - Choose clear, recognizable Lucide icons that represent actions intuitively

  3. Implement Smart Grouping - Use submenus and logical grouping for related functionality and complex workflows

  4. Provide Rich Visual Feedback - Use colors, badges, progress indicators, and animations to convey information effectively

  5. Test Cross-Platform - Ensure radial menus work seamlessly on various screen sizes, resolutions, and input methods

  6. Handle Edge Cases - Validate states, permissions, and context before showing menus to prevent errors

  7. Balance Radius Sizing - Use appropriate radius values that balance visibility with screen space efficiency

  8. Optimize Animation Performance - Keep animations smooth and responsive while avoiding performance overhead

  9. Implement Proper Cleanup - Clean up event handlers, timers, and resources when menus are closed

  10. Use Consistent Styling - Maintain visual consistency across different radial menus in your resource

  11. Provide Keyboard Shortcuts - Include keyboard alternatives for accessibility and power users

  12. Test Input Methods - Verify functionality with mouse, keyboard, and gamepad controllers

  13. Implement Context Awareness - Use condition functions and dynamic items based on player state and permissions

  14. Consider Mobile Users - Ensure touch-friendly interactions and appropriate sizing for mobile devices

  15. Monitor Performance - Track radial menu usage and optimize based on player behavior and system performance

Troubleshooting

Radial Menu Not Showing

  1. Configuration Issues

    • Verify the options parameter is provided and contains valid data structure

    • Check that items array contains properly formatted item objects with required properties

    • Ensure radius values are positive numbers within reasonable ranges (50-300px)

    • Validate that all required properties (label, value, icon) are present in items

  2. Display Problems

    • Ensure no other modal dialogs or UI elements are blocking the display

    • Check for conflicting z-index values with other UI components

    • Verify the radial menu isn't being hidden immediately after being shown

    • Test with fixed positioning and simple configurations first

  3. Debug Information

    • Check console for error messages from B2Lib.Debug system

    • Enable debug mode in configuration to see detailed radial operations

    • Verify B2Lib is properly initialized and all dependencies are loaded

    • Test with minimal radial configurations to isolate issues

Selection and Interaction Issues

  1. Event Handler Problems

    • Verify event handlers are registered before showing radial menus

    • Check that item values are unique within each radial menu

    • Ensure event handler functions are properly defined and accessible

    • Test event firing with console logging to verify event flow

  2. Input Blocking

    • Ensure mouse/controller input is not blocked by other scripts or UI elements

    • Check for conflicting control disabling from other resources

    • Verify deadzone settings are appropriate for your input method

    • Test with different input devices (mouse, gamepad, touch)

  3. Selection Sensitivity

    • Adjust selectionSensitivity configuration for better responsiveness

    • Check that selection deadzone is not too large or too small

    • Verify mouse cursor positioning is accurate and responsive

    • Test selection with different radius sizes and item counts

Performance and Animation Issues

  1. Frame Rate Problems

    • Avoid too many items in a single radial menu (recommended max: 12)

    • Optimize item data calculations and avoid expensive operations in condition functions

    • Use reasonable animation durations (200-400ms) to balance smoothness and responsiveness

    • Monitor CPU usage during radial menu operations

  2. Memory Management

    • Clean up event handlers when radial menus are no longer needed

    • Avoid storing large objects in context data or item properties

    • Implement proper resource cleanup on resource stop/restart

    • Monitor memory usage during extended radial menu usage

  3. Animation Smoothness

    • Use CSS transforms instead of changing position properties for better performance

    • Implement proper easing functions for natural-feeling animations

    • Avoid animating too many properties simultaneously

    • Test animations on lower-end hardware to ensure compatibility

Visual and Styling Issues

  1. Icon and Color Problems

    • Check that icon names are valid Lucide icons and properly spelled

    • Verify color values are in correct format (#hex, rgba, or named colors)

    • Test with different theme modes (dark/light) to ensure proper contrast

    • Ensure custom styling doesn't conflict with default B2Lib styles

  2. Layout and Positioning

    • Ensure radius values are appropriate for screen size and item count

    • Check that labels don't overlap with items or extend outside screen bounds

    • Verify center circle sizing is proportional to overall radial size

    • Test positioning on different screen resolutions and aspect ratios

  3. Theme Integration

    • Verify theme colors are properly applied and accessible

    • Check contrast ratios for accessibility compliance

    • Test with custom themes and ensure proper fallback behavior

    • Ensure theme switching works correctly during runtime

Server-Side Integration Issues

  1. Network Synchronization

    • Account for network latency when showing server-side radial menus

    • Implement proper error handling for failed network operations

    • Use appropriate timeouts for server-side radial menu operations

    • Verify player connection status before sending radial menu data

  2. Permission Validation

    • Verify player exists and is connected before showing server-side radials

    • Implement proper permission checks on both client and server sides

    • Handle permission changes during active radial menu sessions

    • Validate context data on server side to prevent exploitation

  3. Resource Conflicts

    • Check for conflicts with other resources using radial menus

    • Ensure proper resource dependencies are declared in fxmanifest.lua

    • Test radial menu functionality after resource restarts and updates

    • Monitor for conflicts with other UI systems and input handlers

Last updated