Skip to content

UI

Add custom widgets to the menu. Every widget call takes (tab, section, name, ...). Create the tab first with ui.new_tab(); a widget call on a missing tab raises "tab does not exist". Sections are created on first use.

Every widget call returns a ref string of the form "tab|section|widget_id". You don't get the value back directly. Read and write through ui.get(ref) and ui.set(ref, value). State is persisted between sessions automatically.

widget_id is the name you passed, unless two widgets in one section share it: the first keeps the bare name, later ones get #2, #3 and so on. ui.get(tab, sec, "Enabled") addresses the first; reach the others through the returned ref or the suffixed id. Saved config keys use the same id, so adding or removing a duplicate label shifts which stored value each widget gets.

Tab and section names must not contain |. A ref is split on its first two, so a | in either breaks parsing. A | inside a widget name is fine.

Quick example

lua
ui.new_tab("My ESP", "My ESP")

local on        = ui.checkbox("My ESP", "Boxes", "Enabled", true)
local thickness = ui.slider_int("My ESP", "Boxes", "Thickness", 1, 5, 2)
local color     = ui.color_picker("My ESP", "Boxes", "Color")

ui.button("My ESP", "Boxes", "Reset", function()
    print("reset clicked")
end)

function on_tick()
    if not ui.get(on) then return end
    for _, p in ipairs(get_players()) do
        if p:is_enemy() and p:is_alive() then
            local b = p:screen_box()
            if b then
                draw.rect(b.x, b.y, b.w, b.h,
                          ui.get(color), ui.get(thickness))
            end
        end
    end
end

The first three arguments ("My ESP", "Boxes", the widget name) determine where the widget appears in the menu. Reuse the same tab+section across calls to group widgets together.

Custom tab labels

ui.new_tab(id, display) takes a separate group id and display label. The id (the first argument to every widget call) groups widgets and stays fixed in code; the display label is what shows in the menu's tab bar and can have spaces or special characters. Pass a distinct display label when you want the two to differ:

lua
ui.new_tab("toast_demo", "Toast Demo")

ui.button("toast_demo", "styles", "Default", function()
    toast("Hi", 2)
end)
FunctionReturnsDescription
ui.new_tab(id, label)-Register a tab. The internal id (used by every widget call's first argument) can differ from the display label shown in the menu. Must be called before any widget call that targets this tab. If you use the same string for both id and label, calling ui.new_tab(id, id) is still required

Windows

FunctionReturnsDescription
ui.new_window(id, label, [icon])-Register a floating window instead of a sidebar tab. Widgets attach to it exactly like a tab: pass the same id as their first argument. icon is a preset glyph name (default "gear") drawn as a navbar button that toggles the window

A window tab does not appear in the menu sidebar. It starts closed. The user opens it from its navbar button, or closes it with the window's X. Position and size are remembered between launches. Like ui.new_tab, re-registering an id already owned by a different loaded script raises an error.

Preset icon names: gear, wrench, star, crosshair, eye, bolt, code, wand, shield, user, users, map, brush, chart, clock, bell, lock, flask, fire, heart, list, sliders. Any unrecognised name falls back to gear.

lua
ui.new_window("radar_win", "Radar", "map")
local on = ui.checkbox("radar_win", "General", "Enabled", true)

Widgets

FunctionReturnsDescription
ui.checkbox(tab, section, name, [default], [in_line])refToggle. default is a bool; in_line places the widget inline with the previous one
ui.slider_int(tab, section, name, min, max, [default])refInteger slider. default falls back to min when omitted, not to 0
ui.slider_float(tab, section, name, min, max, [default])refFloat slider. default falls back to min when omitted, not to 0
ui.dropdown(tab, section, name, options, [default_index])refDropdown. options is an array of strings; selection returned as 1-based index
ui.multiselect(tab, section, name, options)refMulti-select. ui.get returns a boolean array indexed by option position (1-based)
ui.color_picker(tab, section, name, [default], [in_line])refColor picker. default is an optional {r,g,b,a} Color table; ui.get returns a Color table {r,g,b,a} (0-255)
ui.input_text(tab, section, name, [default])refText input
ui.keybind(tab, section, name, [default_vk])ref"Click to bind" keybind picker. Stored as VK code. default_vk defaults to 0x06 (VK_XBUTTON2) when omitted
ui.font_picker(tab, section, name, [default_font])refFont picker with system font search. Stored as font name string. See Draw > Fonts
ui.button(tab, section, name, callback)refButton. callback runs when pressed
ui.label(tab, section, text)refStatic wrapped text label. No interactive value

Each widget also has a ui.new_* alias (ui.new_checkbox, ui.new_slider_int, etc.) for consistency with older script styles. The aliases are identical to the unprefixed versions.

One exception

ui.new_label does not exist, only ui.label. Calling it errors with "attempt to call a nil value (field 'new_label')".

Reading and writing values

FunctionReturnsDescription
ui.get(ref)variesCurrent value of the widget. Also accepts (tab, sec, name) directly instead of a ref string. Type depends on the widget
ui.get_value(ref)variesAlias for ui.get
ui.set(ref, value)-Override the widget's current value. Also accepts (tab, sec, name, val) directly. Each call flags the owning script for a full config write after the tick, so don't call it every frame on a persisted widget. Write once, on the frame the value settles. Values for slider_int / dropdown / keybind must be whole numbers; a fractional number stores 0
ui.set_value(ref, value)-Alias for ui.set
ui.set_visibility(ref, bool)-Show or hide the widget. Also accepts (tab, sec, name, visible) directly
lua
local mode = ui.dropdown("Misc", "General", "Mode", {"Off", "Casual", "Tryhard"}, 1)

function on_tick()
    if ui.get(mode) == 3 then
        -- Tryhard mode
    end
end

-- Set programmatically:
ui.set(mode, 2)

Use ui.set_visibility for conditional UI without re-creating widgets:

lua
local advanced = ui.checkbox("Misc", "General", "Advanced mode")
local detail   = ui.slider_int("Misc", "General", "Detail level", 1, 10, 5)

function on_tick()
    ui.set_visibility(detail, ui.get(advanced))
end

Sections

ui.new_section creates a named card (section) inside a tab, giving you control over layout options that are not available when sections are created implicitly by widget calls.

FunctionReturnsDescription
ui.new_section(tab, ref, display, [opts])-Create a section card. If tab does not exist, silently does nothing. Returns nothing: address the section by its ref string in later widget calls
ui.new_container(tab, ref, display, [opts])-Alias for ui.new_section

opts keys, all optional:

KeyTypeDefaultEffect
halfsizebooleanfalseHalf width, so two adjacent half-size sections share a row
responsivebooleanfalseA half-size section falls back to full width when the container is narrower than 380 px
boxedbooleantrueDraw the menu card (title, underline, background). boxed = false is the opt-out
collapsiblebooleanfalseUse the older collapsing-header look instead of the card
iconstring-Preset glyph name for the panel header. Glyph presets only. Art keys work on rows (ui.set_icon), not on section headers
gearstring-Ref of another section to show inside this panel header's cog. See ui.section_gear
subtabinteger-1Which ui.tabs page this section belongs to, 0-based. -1 keeps it visible on every page
visiblebooleantrueHide the whole section

autosize and next are accepted and stored, but nothing reads them. They have no effect.

Options are applied only when the section is first created. A section auto-creates on the first widget call that names it, so ui.new_section after that call is a no-op. Declare the section before its widgets.

lua
ui.new_section("My Tab", "main_sec", "Options", { halfsize = true })
local on = ui.checkbox("My Tab", "main_sec", "Enable")

Page tabs

FunctionReturnsDescription
ui.tabs(tab, labels, [icons])-Add a page-level tab strip inside a script tab, splitting its sections across pages. labels is a required array of strings; icons is an optional parallel array of preset glyph names, and an empty or missing entry draws no icon rather than a fallback cog

Sections opt into a page with the subtab option on ui.new_section. Indices are 0-based (first label = subtab = 0). A section left at the default subtab = -1 stays visible on every page. That is how you keep a common header above the strip. Call this after the tab exists. It silently does nothing otherwise.

lua
ui.new_tab("my_esp", "My ESP")
ui.tabs("my_esp", { "Visuals", "Aim" }, { "eye", "crosshair" })

ui.new_section("my_esp", "status", "Status")                    -- on every page
ui.new_section("my_esp", "boxes",  "Boxes", { subtab = 0 })
ui.new_section("my_esp", "aimbot", "Aim",   { subtab = 1 })

Rows

FunctionReturnsDescription
ui.begin_row(tab, section)-Start a row group: every widget created until ui.end_row shares one row and gets an equal slice of the section width
ui.end_row(tab, section)-Close the row group opened by ui.begin_row; widgets created afterwards get their own full-width rows again

Both resolve an existing section and silently do nothing if it does not exist yet. Sections auto-create on their first widget call, so create the section (ui.new_section, or one widget) before calling ui.begin_row.

Rows are grouped by adjacency. Hiding a widget in the middle of a row with ui.set_visibility ends the row at that point rather than closing the gap.

lua
ui.new_section("My Tab", "aim", "Aim")
ui.begin_row("My Tab", "aim")
ui.slider_int("My Tab", "aim", "FOV", 1, 180, 30)
ui.slider_float("My Tab", "aim", "Smoothing", 0.1, 1.0, 0.5)
ui.end_row("My Tab", "aim")

Row decoration

FunctionReturnsDescription
ui.set_icon(ref, icon, [color])-Put an icon on this widget's row. Also accepts (tab, sec, name, icon, [color]). icon is either a preset glyph name ("crosshair", "eye", ...) or an art key: the file name of a loaded icon such as "Infernus" or "Healing_Rite". That choice is made when the row is drawn, not when you call this, so art keys still work if the icon atlas is still loading. color is an optional {r, g, b} table (0-255) that tints preset glyphs only; art icons draw untinted, and alpha is always 255
ui.set_sub(ref, [enabled], [inert])-Indent this widget's row underneath the one above it, marking it as a child setting. Also accepts (tab, sec, name, [enabled], [inert]). enabled defaults to true, so ui.set_sub(ref) turns indenting on; pass false to turn it off. inert = true also greys the row out and swallows clicks. Consecutive sub rows share one continuous indent rail
lua
local on  = ui.checkbox("My Tab", "aim", "Aimbot", true)
ui.set_icon(on, "crosshair")

local fov = ui.slider_int("My Tab", "aim", "FOV", 1, 180, 30)
ui.set_sub(fov)                 -- indented under the toggle above

Gear sections

A cog button can host an entire section, either on a widget's row or in a section's panel header.

FunctionReturnsDescription
ui.set_gear(ref, gear_section)-Host an entire section inside a cog button on this widget's row. This is the usual way to tuck advanced options behind the toggle they belong to. Also accepts (tab, sec, name, gear_section). gear_section is the ref of a section in the same tab
ui.section_gear(tab, section, gear_section)-Same idea as ui.set_gear, but the cog lives in the section's panel header instead of on a widget row. All three arguments are required

The hosted section is relocated, not copied: it stops rendering in the normal section flow. That flag is set when ui.set_gear / ui.section_gear runs, so create the gear section (or at least its first widget) before calling this. Otherwise it draws both inline and inside the cog. Silently does nothing if the widget or the section can't be found.

The gear option on ui.new_section only points the header cog at a section. It does not relocate it, so that section would render both inline and inside the cog. Use ui.section_gear, or create the hosted section with { visible = false }.

lua
ui.new_section("My Tab", "aim_adv", "Advanced")      -- declare it first...
ui.slider_float("My Tab", "aim_adv", "Smoothing", 0.1, 1.0, 0.5)

local on = ui.checkbox("My Tab", "aim", "Aimbot", true)
ui.set_gear(on, "aim_adv")                           -- ...then tuck it behind the cog

Tooltips

FunctionReturnsDescription
ui.set_tooltip(ref, text)-Attach hover tooltip text to a widget. Also accepts (tab, sec, name, text) directly
ui.tooltip(ref, text)-Alias for ui.set_tooltip
lua
local spd = ui.slider_float("Aim", "General", "Speed", 0.1, 1.0, 0.5)
ui.set_tooltip(spd, "Lower = smoother. 1.0 = instant snap.")

Theme and menu state

FunctionReturnsDescription
ui.accent()Color {r,g,b,a}Current menu accent color (matches the theme). a is always 255
ui.accent_color()Color {r,g,b,a}Alias for ui.accent
ui.is_menu_open()booleanTrue while the overlay menu is open. Use this to gate panel dragging so panels cannot be moved during gameplay
ui.menu_open()booleanAlias for ui.is_menu_open

Draggable and resizable panels

Call these each tick (inside on_tick) to let users reposition and resize script-drawn panels while the menu is open.

FunctionReturnsDescription
ui.draggable(id, x, y, w, h)nx, ny, draggingDrag a panel. id must be unique per panel. Returns the (possibly updated) top-left position and a dragging flag. Persist nx, ny yourself (e.g. via ui.set on release)
ui.resizable(id, x, y, w, h, value, [min], [max])new_value, resizing, factorProportional resize grip (bottom-right corner). value is the size parameter to scale; factor is the raw drag ratio, apply it to other size params for uniform scaling. Call before ui.draggable on the same panel so the grip grab takes priority. Inert while the menu is closed
lua
local px = ui.slider_int("Radar", "Panel", "X", 0, 3840, 100)
local py = ui.slider_int("Radar", "Panel", "Y", 0, 2160, 100)
local ps = ui.slider_int("Radar", "Panel", "Size", 50, 800, 300)

function on_tick()
    if not ui.is_menu_open() then return end
    local x, y, s = ui.get(px), ui.get(py), ui.get(ps)

    -- resize grip first so it wins over the body drag
    local ns, resizing = ui.resizable("radar_panel", x, y, s, s, s, 50, 800)
    local nx, ny, dragging = ui.draggable("radar_panel", x, y, s, s)

    -- ... draw the panel at nx, ny with size ns ...

    -- Persist once, on the release frame: both helpers report their flag false
    -- and hand back the final value on the frame the mouse comes up.
    if not resizing and ns ~= s then ui.set(ps, math.floor(ns + 0.5)) end
    if not dragging and (nx ~= x or ny ~= y) then
        ui.set(px, math.floor(nx + 0.5))
        ui.set(py, math.floor(ny + 0.5))
    end
end

Return value types

Widgetui.get returns
checkboxboolean
slider_int, keybindinteger
slider_floatnumber
dropdowninteger (1-based index)
multiselecttable of booleans (indexed by option position; true = selected)
color_pickerColor table {r, g, b, a} (0-255 per channel)
input_text, font_pickerstring
buttonnothing meaningful (use the callback)
labelnothing meaningful (display-only)

settings vs ui

For most scripts, declare config in the settings = { ... } table instead. It's simpler, auto-renders below your script in the menu, and uses the config.* namespace for reading/writing.

Use ui.* when you need custom tabs, button callbacks, or runtime-mutable visibility via ui.set_visibility.