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
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
endThe 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:
ui.new_tab("toast_demo", "Toast Demo")
ui.button("toast_demo", "styles", "Default", function()
toast("Hi", 2)
end)| Function | Returns | Description |
|---|---|---|
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
| Function | Returns | Description |
|---|---|---|
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.
ui.new_window("radar_win", "Radar", "map")
local on = ui.checkbox("radar_win", "General", "Enabled", true)Widgets
| Function | Returns | Description |
|---|---|---|
ui.checkbox(tab, section, name, [default], [in_line]) | ref | Toggle. default is a bool; in_line places the widget inline with the previous one |
ui.slider_int(tab, section, name, min, max, [default]) | ref | Integer slider. default falls back to min when omitted, not to 0 |
ui.slider_float(tab, section, name, min, max, [default]) | ref | Float slider. default falls back to min when omitted, not to 0 |
ui.dropdown(tab, section, name, options, [default_index]) | ref | Dropdown. options is an array of strings; selection returned as 1-based index |
ui.multiselect(tab, section, name, options) | ref | Multi-select. ui.get returns a boolean array indexed by option position (1-based) |
ui.color_picker(tab, section, name, [default], [in_line]) | ref | Color 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]) | ref | Text 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]) | ref | Font picker with system font search. Stored as font name string. See Draw > Fonts |
ui.button(tab, section, name, callback) | ref | Button. callback runs when pressed |
ui.label(tab, section, text) | ref | Static 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
| Function | Returns | Description |
|---|---|---|
ui.get(ref) | varies | Current value of the widget. Also accepts (tab, sec, name) directly instead of a ref string. Type depends on the widget |
ui.get_value(ref) | varies | Alias 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 |
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:
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))
endSections
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.
| Function | Returns | Description |
|---|---|---|
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:
| Key | Type | Default | Effect |
|---|---|---|---|
halfsize | boolean | false | Half width, so two adjacent half-size sections share a row |
responsive | boolean | false | A half-size section falls back to full width when the container is narrower than 380 px |
boxed | boolean | true | Draw the menu card (title, underline, background). boxed = false is the opt-out |
collapsible | boolean | false | Use the older collapsing-header look instead of the card |
icon | string | - | Preset glyph name for the panel header. Glyph presets only. Art keys work on rows (ui.set_icon), not on section headers |
gear | string | - | Ref of another section to show inside this panel header's cog. See ui.section_gear |
subtab | integer | -1 | Which ui.tabs page this section belongs to, 0-based. -1 keeps it visible on every page |
visible | boolean | true | Hide 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.
ui.new_section("My Tab", "main_sec", "Options", { halfsize = true })
local on = ui.checkbox("My Tab", "main_sec", "Enable")Page tabs
| Function | Returns | Description |
|---|---|---|
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.
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
| Function | Returns | Description |
|---|---|---|
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.
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
| Function | Returns | Description |
|---|---|---|
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 |
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 aboveGear sections
A cog button can host an entire section, either on a widget's row or in a section's panel header.
| Function | Returns | Description |
|---|---|---|
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 }.
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 cogTooltips
| Function | Returns | Description |
|---|---|---|
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 |
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
| Function | Returns | Description |
|---|---|---|
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() | boolean | True while the overlay menu is open. Use this to gate panel dragging so panels cannot be moved during gameplay |
ui.menu_open() | boolean | Alias 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.
| Function | Returns | Description |
|---|---|---|
ui.draggable(id, x, y, w, h) | nx, ny, dragging | Drag 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, factor | Proportional 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 |
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
endReturn value types
| Widget | ui.get returns |
|---|---|
checkbox | boolean |
slider_int, keybind | integer |
slider_float | number |
dropdown | integer (1-based index) |
multiselect | table of booleans (indexed by option position; true = selected) |
color_picker | Color table {r, g, b, a} (0-255 per channel) |
input_text, font_picker | string |
button | nothing meaningful (use the callback) |
label | nothing 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.