Argon UI v2.0.0 StartWhat's newComponentsThemesAPI PreviewGitHub
Roblox Luau UI library · 2.0

Argon UI

A single-file UI library for Roblox Luau scripts. It gives you a window with tabs, sections and 22 control builders, a command palette, change tracking with undo, configuration profiles, notifications, twelve themes, and full mouse, keyboard, touch and gamepad support. The API follows Rayfield's, so most existing menus only need their loader swapped.

One file, no dependencies22 control builders12 themes Command paletteProfiles and undoMouse, keyboard, touch, gamepadRayfield-style API

Getting started

Argon UI is one self-contained Luau file. It needs no host script, no folder layout and no globals of your own. Load it, create a window, add a tab, and add controls.

local Argon = loadstring(game:HttpGet(
    "https://raw.githubusercontent.com/JirkaachS/argon-ui/main/argon-ui.luau"
))()

local Window = Argon:CreateWindow({
    Name = "My Hub",
    Theme = "Argon",
    ConfigurationSaving = { Enabled = true, FolderName = "MyHub", FileName = "config" },
})

local Main = Window:CreateTab("Main", "zap", { Group = "Farming" })
Main:CreateSection("Auto farm")

Main:CreateToggle({
    Flag = "AutoFarm",
    Name = "Auto farm",
    Description = "Collects drops and finishes quest steps.",
    CurrentValue = false,
    Keybind = "F",   -- optional: pressing F flips it
    Callback = function(enabled)
        print("auto farm:", enabled)
    end,
})

Window:CreateLibrarySettings(Window:CreateTab("Settings", "settings"))
Argon:LoadConfiguration()
That last line matters. LoadConfiguration restores saved values, runs each restored control's callback once, and turns autosave on. Call it once, after your controls are built. You do not need to open the window yourself: it opens when your script finishes.

Installation

Remote (recommended)

local Argon = loadstring(game:HttpGet(
    "https://raw.githubusercontent.com/JirkaachS/argon-ui/main/argon-ui.luau"
))()

Local file with remote fallback

Useful when you ship the library beside your script and still want updates.

local function loadArgon()
    if isfile and readfile and isfile("argon-ui.luau") then
        local chunk = loadstring(readfile("argon-ui.luau"), "@argon-ui.luau")
        if chunk then return chunk() end
    end
    return loadstring(game:HttpGet(
        "https://raw.githubusercontent.com/JirkaachS/argon-ui/main/argon-ui.luau"
    ))()
end

local Argon = loadArgon()
Environment. The window goes into gethui(), CoreGui or PlayerGui, whichever is available. After the one HttpGet that fetches the file, the library makes no network requests while it loads: all 1,573 icons are built in and the avatar is an rbxthumb:// URL. Configuration saving needs the executor's file functions (writefile, readfile, isfile); without them the interface works and simply does not persist.

The source lives in src/ as about 80 small modules. build.luau bundles them into the single argon-ui.luau you load, so loadstring(game:HttpGet(url))() keeps working.

What's new in 2.0

Argon UI 2 is a rebuild. The public API is source-compatible with 1.18: every method, alias, option key, callback payload and saved-value format still works, and 1.x configuration files load unchanged. What is different:

Speed

Nothing slow before the first frame

No icon download, no avatar lookup and no base64 decode at load. Create* records state; the visible tab is built first in small slices, the rest after the window opens. Nothing runs while idle.

Loader

A loader without fixed timers

A quick script opens with no loader card. A big one shows what is really happening, "Building Combat · 155 of 212", and opens when the work is done.

Look

Glass frame, solid content

A tinted frame around an opaque sheet, one platter per section, Montserrat headings over Roboto rows, 12 themes, an accent override, three materials, scale and density.

Find

Command palette

Ctrl+K, / or gamepad Y. Finds controls in tabs that were never opened; Enter goes, Ctrl+Enter runs in place, Left and Right adjust.

Undo

Change tracking and undo

Changed rows get a dot, tabs and sections reset, and Ctrl+Z or the Undo pill brings the values back.

Menu

Row menu

Right-click, long-press, Shift+F10 or gamepad X on any row: reset, copy and paste the value, copy the flag, pin, show in palette.

Profiles

Named configurations

Save, load, rename and delete profiles, pick one to load on start, and share them as an ARGON2: code.

Notify

Notifications with actions

Types, up to two buttons, progress, update in place, an inbox behind the bell, and errors from your callbacks shown with Copy error and Retry.

Pins

Pins and quick dock

Pinned switches and buttons stay reachable in the sidebar and in a dock that remains on screen while the window is hidden.

Input

Keyboard and gamepad

A focus ring, arrow-key navigation and adjustment, Ctrl+1-9 for tabs, a gamepad hint bar. Backspace and pad B close the top layer.

Controls

New blocks

Callout, Stat with a sparkline, Image, ButtonGroup, Checkbox, button variants and Keybind = "F" on toggles and buttons.

Gate

Key system

Rayfield's KeySystem and KeySettings, plus Validate and GetKeyLink, shown as a card before the window opens.

The complete list is in the changelog, and measured numbers against 1.x are in bench/RESULTS.md.

Upgrading from 1.x

Replace the file. Your script and your users' saved configuration files keep working. These are the changes you might notice; each one is deliberate:

  1. There is no pill loader and no 0.93 s minimum. A quick script opens with no loader card, and the window opens when your script is done.
  2. The player list and keybind list overlays are off unless you enable them (PlayerList = true, KeybindList = true, or the setters). The keybind list shows every bind (active ones highlighted) and hides when there are none.
  3. A Keybind whose config has Callback but no OnPressed/Pressed now calls Callback() on press, as Rayfield does (with HoldToInteract, Callback(true) on press and Callback(false) on release). Rebinds fire OnChanged(key). Scripts that used Callback as a rebind listener should add CallOnChange = true or use OnChanged. Configs with OnPressed keep the 1.x behaviour.
  4. Escape no longer clears a keybind during capture (Backspace or Delete do) and is never a close key, because Escape opens Roblox's own menu. Backspace and gamepad B close the top layer.
  5. A notification's Type overrides the guess from its title, and the guess now matches whole words ("No errors found" is not an error).
  6. No ALL CAPS: section headers, the subtitle and loader status show your text as given. The "LIVE" pill and the "ARGON / TAB" eyebrow are gone (Window:SetStatus adds a status chip if you want one).
  7. Dropdown:Set("unknown") keeps the current selection instead of clearing it.
  8. Input fires its callback on Enter even when the text did not change, and RemoveTextAfterFocusLost fires once and keeps the submitted text as CurrentValue.
  9. Controls in a row no longer pad each other to equal height; _balance() and _relayout() are no-ops.
  10. A slider suffix that is a word gets a space ("10 Bananas"); short suffixes stay attached ("50%", "3ms").
  11. PruneRegistries() returns zero counts: there are no global registries left to prune. Argon._themeBindings and Argon._connections stay as empty tables.
  12. The library settings rows are views over the window preferences instead of flags. The 1.x ArgonUI* flag names still work through GetFlagValue and SetFlagValue.
  13. Window size: the default is max(min(gui - 160, 1080), min(720, gui - 16)) wide by max(min(gui - 110, 700), min(440, gui - 16)) tall; the sidebar is 232 px and collapses to a 64 px icon rail.
  14. LoadConfiguration() returns true (with an info string) when there is no saved file yet, and when every value is parked for controls that do not exist yet.
  15. SetTheme(table) is remembered as a custom theme across sessions. Theme changes made from the picker are saved.
  16. OnFlagChanged unbinds by identity, so unbinding one listener never removes another.
  17. Element:Destroy() releases the flag (its last value is kept for the next save) and any keybinds the control owned.
  18. SetVisible(false) survives tab switches and search.
  19. Lock and SetDisabled block every user path, including a control's keybind; a script's Set still works.
  20. Closing a confirmation with x, the close key or an outside click calls OnCancel.
  21. Colour values read from configuration files without the __argonColor marker are treated as 0-255 (Rayfield's format).
  22. Reading a colour from an instance of a hidden tab right after a theme switch can return the old colour until that tab is shown; read Argon:GetColor or Argon:GetPalette() instead of instance colours.
  23. Overlay transparency (0.12-0.8 in 1.x) now maps onto the frosted HUD range, so overlays stay readable.
  24. SetSmoothTransitions(false) turns on reduced motion.
  25. Technical keeps Sarpanch; the default typeface is still Roboto with Montserrat headings (BuilderSans is one of the six sets).
Palette keys. The 1.x theme keys Background, Window, Sidebar, Input, AccentLight and AccentDark still work as aliases of the 2.0 tokens, for reading (GetColor, OnThemeChanged) and for writing (AddTheme, SetTheme(table)). See Themes.

Windows

Argon:CreateWindow(options) returns a window object straight away. It does not wait on the network and builds only a handful of instances; the shell and the visible tab are built in small slices during the next frames.

OptionTypeDefaultDescription
Namestring"Argon UI"Window name, shown in the sidebar, on the loader card and in messages such as "My Hub is hidden".
Subtitlestring"Control Center"Line under the name, shown as given (no upper-casing).
Logostring or numberfirst letter of the namerbxassetid://… or an asset id. Rayfield's Icon also works.
Themestring or table"Argon"A built-in theme, a Rayfield theme name, or a palette table. See Themes.
Fontstring"Roboto"Typeface set. See Appearance.
ConfigurationSavingtabledisabled{ Enabled, FolderName, FileName, FileExtension }.
GuiNamestring"ArgonUI"ScreenGui name. Only a previous Argon window with this name is replaced. ScreenGuiName also works.
DisplayOrdernumber100The ScreenGui's DisplayOrder.
ToggleUIKeybindKeyCode or stringInsertHides and shows the window. "None" turns the key off.
OpenButton"auto", "always", "off""auto"What brings a hidden window back. Touch devices get a reopen orb. On PC, "auto" shows the dock pill only when there is no toggle key; "always" always shows it.
MouseUnlockbooleantrueFrees the cursor in first-person and shift-lock games while the window is open.
Cursorboolean or "auto""auto"Draws a pointer above the interface. "auto" only does so when the game has hidden the engine cursor.
Scalenumber or "auto""auto"Interface scale, 0.75 to 1.5. "auto" is 1 below a 1,300 px tall screen and grows with taller screens.
Densitystring"Auto""Compact" or "Touch". Auto follows the input device.
ReducedMotionbooleanfalseSprings become instant, fades are capped at 100 ms and loops stop.
ThemePickerbooleantrueThe Appearance button in the topbar.
Palettebooleantruefalse turns the command palette off.
PlayerList, KeybindList, WatermarkbooleanfalseOverlays. All three are off unless you turn them on. See Overlays.
OverlaysAlwaysVisiblebooleanfalseKeeps the overlays on screen while the window is hidden.
Loading"manual"automaticHold the window until you call FinishLoading. ManualLoading = true also works. See Loading.
LoadingScreenstring or table"auto""auto", "always", "never" or { Title, Subtitle, Tips }. Rayfield's LoadingTitle and LoadingSubtitle map onto it.
LoadingMinimumnumber0.25Seconds the loader card stays once it has appeared, so it never flickers.
KeySystem, KeySettingsboolean, tableoffRayfield's key gate. See Key system.
PrewarmbooleantrueBuilds the other tabs in the background after the window opens. false builds each tab the first time it is shown.

Size and layouts

The default size is max(min(gui - 160, 1080), min(720, gui - 16)) wide by max(min(gui - 110, 700), min(440, gui - 16)) tall, centred. The minimum is 560×400. Drag the topbar or the brand to move the window and the grip at the bottom-right to resize it; position and size are remembered. Window:SetSize(Vector2) sets it from code.

LayoutWhenShape
Widewindow at least 1,080 px wide232 px sidebar, page column, and an "On this page" list on the right
Standard760 to 1,080 px232 px sidebar, page column, and a row of section chips under the page title
Icon railunder 760 px, or the user collapsed the sidebara 64 px rail of icons; tooltips name each tab
Phone landscapetouch, short side under 500 pxthe window fills the screen, with the rail and touch-sized rows
Phone portraittouch, screen under 600 px widea full-screen sheet with a bottom tab bar; pickers open as bottom sheets

Showing and hiding

Insert (or your ToggleUIKeybind) hides and shows the window. The first time a user hides it, a notice says how to bring it back ("My Hub is hidden. Press Insert to reopen"). While it is hidden, touch devices get a reopen orb on the left edge and PC gets a dock pill when OpenButton asks for one; both can be dragged and snap to the nearest edge. Pinned controls sit next to them.

Window:SetVisibility(false)      -- also Open(), Close(), Toggle(), ToggleVisibility()
print(Window:IsVisible())
Window:SetToggleKey(Enum.KeyCode.RightShift)
Window:SetOpenButton("always")   -- "auto", "always" or "off"
Window:SetMouseUnlock(false)     -- keep the cursor locked in first-person games

Cursor

Some games draw their own pointer: they set UserInputService.MouseIconEnabled to false and render an image in PlayerGui. PlayerGui sits below CoreGui, so that pointer disappears behind the window and you end up clicking blind. When the engine cursor is off, Argon draws its own above everything it owns.

Argon:CreateWindow({ Name = "My Hub", Cursor = "auto" })  -- default
Window:SetCursorVisible(false)   -- never draw one
Window:SetCursorVisible(true)    -- always draw one
Window:SetCursorVisible()        -- back to automatic detection
print(Window:IsCursorDrawn())

Loading

You do not have to do anything. While your script builds, the window stays hidden; it opens as soon as one of these is true: your script finished, it called FinishLoading, or it has built a tab and gone quiet for 250 ms. Nothing is shown for the first 150 ms, so a quick script opens with no loader card at all. A card appears only while real work is still pending, says what it is doing, and hands over to the already-built window with a short morph.

Your scriptWhat happens
Builds everything in one goThe window opens in about 0.1 to 0.2 s with no card.
Builds, then calls LoadConfigurationThe card appears at 150 ms showing "Restoring settings · 72 of 96" and the window opens when the restore ends.
Waits (an HttpGet, WaitForChild) before creating any tabThe card shows "Waiting for script"; after 8 s without progress it offers Open now and Copy log.
Waits in the middle of the buildAfter 250 ms of quiet the window opens with the tabs built so far; later tabs slide into the sidebar.
Uses Loading = "manual" and never finishesThe stall footer appears at 8 s, and the window opens by itself 30 s after the last progress report.

Driving the loader yourself

Declare manual mode in the options, report progress, and finish. Progress never goes backwards: a lower value than the last one is ignored.

local Window = Argon:CreateWindow({ Name = "My Hub", Loading = "manual" })

for index, spec in ipairs(tabsToBuild) do
    Window:SetLoadingProgress(index / #tabsToBuild, "Building " .. spec.Name)
    buildTab(spec)
end

Argon:LoadConfiguration()
Window:FinishLoading()   -- the window opens with the restored values already on screen
-- Runs your builder under xpcall: an error is shown on the loader card
-- with Open anyway and Copy error instead of leaving a half-built window.
Window:Build(function(window)
    local Combat = window:CreateTab("Combat", "swords")
    -- ...
end)

Window:OnLoaded(function()
    Argon:Notify({ Title = "Ready", Content = "Press Insert to hide the menu.", Type = "info" })
end)
MemberDescription
SetLoadingProgress(fraction, status)Moves the bar and sets the status line. Any call switches the window to manual readiness, so it waits for FinishLoading.
SetLoadingStatus(text)Changes only the status line, shown as given.
FinishLoading()Opens the window. Safe to call more than once.
IsLoading()true until the window has been revealed.
OnLoaded(fn)Runs fn(window) after the hand-over, or right away if the window is already open. Returns a connection.
Build(fn)Runs fn(window) under xpcall. An error is shown on the loader card with Open anyway and Copy error.
Argon:CreateWindow({
    Name = "My Hub",
    LoadingScreen = {                -- or "auto", "always", "never"
        Title = "My Hub",
        Subtitle = "by me",
        Tips = { "Tip: press Ctrl K to search every setting" },
    },
    LoadingMinimum = 0.25,           -- seconds the card stays once shown
})

Key system

An opt-in key gate with Rayfield's option names. The card is drawn before anything waits, then CreateWindow waits for a valid key; this is the one documented yield in CreateWindow. A wrong key shakes the card and shows the reason; a remembered key skips the gate next time.

local Window = Argon:CreateWindow({
    Name = "My Hub",
    KeySystem = true,
    KeySettings = {
        Title = "My Hub",
        Subtitle = "Enter your key to continue",
        Note = "Keys are free: open the link, then paste the key here.",
        FileName = "MyHubKey",       -- remembered as FolderName/MyHubKey.key
        SaveKey = true,
        Key = { "alpha-2024", "beta-2024" },
    },
    GetKeyLink = "https://example.com/key",
    -- Or check keys yourself; return false plus a message to reject:
    -- Validate = function(key) return checkKeyOnServer(key) end,
})

KeySettings.GrabKeyFromSite = true treats each Key entry as a URL and fetches the valid keys in a separate thread. Validate(key) runs in its own thread too, and the Continue button shows "Checking…" while it runs; it returns true to accept, or false, message to reject with your message. Both Validate and GetKeyLink may also be given inside KeySettings.

Tabs and sections

Tabs hold sections, and sections hold controls. If you add a control before creating a section, a General section is created for you.

local Combat = Window:CreateTab("Combat", "swords", {
    Group = "Main",                        -- a caption in the sidebar
    Description = "Aim, targeting and session stats.",
})

Combat:CreateSection("Targeting")
Combat:CreateToggle({ Name = "Aim assist", Callback = function() end })

-- Section objects build controls directly, which reads well for long tabs.
local Movement = Combat:Section({ Title = "Movement", Icon = "move", Collapsible = true })
Movement:Slider({ Name = "Walk speed", Range = { 16, 120 }, CurrentValue = 16 })
Movement:Toggle({ Name = "Infinite jump" })

Combat:SetBadge(3)          -- a count, "New", true for a dot, or nil to clear
Window:SelectTab("Combat")  -- a tab object, its name or its index

Every Create* method also exists without the prefix (Tab:Toggle, Tab:Slider, Section:Dropdown, and so on), so both naming styles work. The third argument of CreateTab is optional, and a table in place of the second argument also works: CreateTab("Combat", { Icon = "swords", Group = "Main" }).

MemberDescription
CreateSection(name) or CreateSection({ Title, Icon, Description, Collapsible, Collapsed })Adds a section: a header and one grouped platter. Section is an alias.
Section:Set(title)Renames the section.
Section:SetCollapsed(bool)Collapses or expands it; also Collapse(), Expand(), ToggleCollapsed(), IsCollapsed(). The state is remembered.
Section:SetVisible(bool)Hides or shows the whole section.
Section:Reset() · Section:GetChangedCount()Resets every changed control in the section; counts them.
Tab:SetBadge(value)A number, "New", true (a dot) or nil.
Tab:Select() · Tab:SetTitle(name)Shows the tab; renames it.
Tab:Reset() · Tab:GetChangedCount()See Changes and undo.

Section navigation

Long tabs get section navigation with nothing to call. In the wide layout an "On this page" list sits to the right of the page and follows the scroll; in the standard, rail and phone layouts a row of section chips sits under the page title once a tab has three or more sections. Clicking either scrolls to that section.

Rows

A row splits a section into equal columns and hands controls out round-robin, so the pair you build together sits side by side. It suits stat tiles, button groups and compact controls such as toggles, sliders, steppers and keybinds; lists and text areas want the full width.

local row = Tab:CreateRow({ Columns = 2 })
row:CreateToggle({ Flag = "Chams", Name = "Chams", CurrentValue = false })
row:CreateSlider({ Flag = "ChamsOpacity", Name = "Opacity %", Range = { 5, 100 }, CurrentValue = 60 })

-- when the pairing matters more than the balance
row:Use(2):CreateToggle({ Name = "Pin me right", CurrentValue = false })

Columns accepts 1 to 4 and may be passed bare (Tab:CreateRow(2)). Spacing sets the gap and MinColumnWidth (default 200) is the width below which the columns stack, so a resized window and a phone get a readable layout from the same calls. On phones rows always stack. Controls in a row keep their own height; 1.x padded them to equal heights, and _balance() and _relayout() are now no-ops.

What it looks like

These are drawings of the interface made with the library's own tokens and metrics, so you can pick a control by sight; each one sits next to the call that produces it further down. They follow this page's colour scheme: the Argon theme in dark mode and Daylight in light mode.

For a version you can click, the browser preview rebuilds the whole interface in HTML: sliders drag, the colour picker has a real HSV square, the palette searches, rows wrap as the window narrows, and all twelve themes apply live.

Only the frame is see-through, and it carries only Text and Text2; everything with dense text sits on the opaque sheet or on opaque platters, so the game behind the window never makes a label hard to read. The accent appears only on what is on, selected, focused or primary.

All components

Twenty-two control builders, grouped by what they do. Anything that holds a value accepts a Flag and takes part in configuration saving. Every card jumps to that control's reference.

value

Toggle

A switch with a check glyph, so state never depends on colour alone.

new · value

Checkbox

A toggle drawn as a checkbox.

value

Slider

A ranged number you drag, step with the keys, or type.

value

Stepper

An exact number with minus and plus buttons.

value

Input

Single-line text with validation, numbers and limits.

value

TextArea

Multi-line text that can grow with its content.

value

Dropdown

Single or multi select, searchable, any number of options.

value

Segmented

Two to four choices side by side.

value

Keybind

Key capture with modes, mouse and pad buttons, and conflicts.

value

ColorPicker

HSV square, hue strip, optional alpha, presets and recents.

action

Button

An action row, or a Primary, Ghost, Danger or Danger soft pill.

new · action

ButtonGroup

Two or three buttons side by side in one row.

display

ProgressBar

Determinate, stepped or indeterminate progress.

display

List

Rows with inline actions, a filter, rename and an empty state.

display

TelemetryCard

A live status line with a coloured status pill.

new · display

Stat

A big number with a unit, a delta pill and a sparkline.

new · display

Callout

A tinted info, success, warning or danger box with an action.

new · display

Image

An image with rounded corners and a caption.

display

Label

A short line of text with an optional icon.

display

Paragraph

A title above wrapped body text.

layout

Divider

A rule with an optional caption.

layout

Space

An explicit vertical gap.

layout

Section

A header and one platter, with its own builders.

layout

Row

Up to four columns inside a section.

Every control config also accepts these keys:

KeyWhat it does
FlagSaves the value under this name and makes it reachable through GetFlagValue and SetFlagValue.
DescriptionA line under the title. Wraps to at most two lines on desktop.
TooltipThe same kind of text without using row height; an info mark after the title shows it on hover, focus or tap.
KeywordsExtra words the command palette matches, e.g. { "esp", "wallhack" }.
PinnedOn toggles and buttons: pins the control to the sidebar and the quick dock.
KeybindOn toggles and buttons: a key that flips or presses it, e.g. Keybind = "F".
ContextMenufalse turns the row menu off for this control.
VisibleWhen / EnabledWhenShows or enables the control only while other flags match, e.g. { Flag = "Mode", Equals = "Advanced" }.
DefaultThe value Reset returns to. Without it, the value at creation time is the default.

Input controls

Toggle

local godMode = Tab:CreateToggle({
    Flag = "GodMode",
    Name = "God mode",
    Description = "Reapplies automatically on respawn.",
    CurrentValue = false,
    Keybind = "G",                 -- optional
    Callback = function(enabled) setGodMode(enabled) end,
})

godMode:Set(true)            -- fires the callback
print(godMode.CurrentValue)  -- true

Click anywhere on the row to flip it. With the keyboard, Space or Enter flips the focused row, Left turns it off and Right turns it on. Style = "Checkbox" draws it as a checkbox. A keybind attached with Keybind obeys Lock and SetDisabled, like every other path.

Checkbox new

Tab:CreateCheckbox({ Flag = "InfiniteJump", Name = "Infinite jump", CurrentValue = true })
-- the same as Tab:CreateToggle({ ..., Style = "Checkbox" })

Input

Tab:CreateInput({
    Flag = "Webhook",
    Name = "Webhook URL",
    PlaceholderText = "https://…",
    Validate = function(text)
        if text ~= "" and not text:match("^https://") then
            return false, "Must start with https://"
        end
        return true
    end,
    Callback = function(value) saveWebhook(value) end,
})

Tab:CreateInput({ Flag = "Range", Name = "Range", Numeric = true, Min = 10, Max = 500 })
OptionDescription
PlaceholderText / PlaceholderShown while empty.
Numeric, Min, MaxNumbers only; out-of-range values clamp on commit and the row says the allowed range.
MaxLengthLimits the length and shows a counter, which turns amber at 90 %.
Validate(text)Return false, "message" to reject the value; the message appears under the row.
LiveSends the draft text to OnChanged listeners while the user types (after a 0.2 s pause). The callback still fires on commit.
ClearButton, Icon, Masked, ReadOnlyA clear button, a leading icon, hidden characters, and a read-only field with a copy button.
RemoveTextAfterFocusLostClears the field after a commit. The callback fires once and CurrentValue keeps the submitted text.

The callback fires when the text changed or when Enter is pressed, even if the text is the same. A long value is shortened in the middle while the field is not focused, and the full text comes back for editing; CurrentValue always holds the whole string.

TextArea

Multi-line entry for lists, notes and payloads. GetLines returns the non-empty lines, which suits "one item per line" input. It always sits under its label at full width.

local filter = Tab:CreateTextArea({
    Flag = "ItemFilter",
    Name = "Item filter",
    PlaceholderText = "One item per line…",
    AutoGrow = true, MinHeight = 80, MaxHeight = 240,
    MaxLength = 500,
    Callback = function(text) print(#text, "characters") end,
})

for _, line in ipairs(filter:GetLines()) do
    print("filter:", line)
end

Height sets a fixed height instead of AutoGrow. Monospace = true uses Roboto Mono for the text. It commits when it loses focus.

Keybind

local panic = Tab:CreateKeybind({
    Flag = "PanicKey",
    Name = "Panic",
    CurrentKeybind = "P",
    HoldToInteract = false,
    Callback = function() panicNow() end,         -- fires on press, as in Rayfield
    OnChanged = function(key) print("rebound to", key) end,
})

panic:Set("K")
print(panic.CurrentKeybind)

Click the cap (or press Enter, Space or pad A on the focused row), then press a key, a mouse button (right and middle work) or a pad button. Backspace or Delete sets it to None. A click outside or the 5 second timeout keeps the old key. Escape is never used: in Roblox it opens the game menu.

Callback timing changed in 2.0. A keybind whose config has Callback but no OnPressed or Pressed now calls Callback() when the key is pressed, as Rayfield does; with HoldToInteract it calls Callback(true) on press and Callback(false) on release. Rebinds fire OnChanged(key). If you used Callback as a rebind listener, add CallOnChange = true or move the code to OnChanged. Configs with OnPressed keep the 1.x behaviour.

Other keys: Mode ("Hold", "Toggle" or "Press", shown next to the cap), Toggle = true to latch, DisplayInKeybindList, and the methods SetActive(bool), IsActive() and SetOverlayVisible(bool). A bind that collides with another one says so under the row and offers Swap (move the other bind to this one's old key) and Keep (both fire).

Button

Tab:CreateButton({
    Name = "Rejoin server",
    Icon = "refresh-cw",
    Shortcut = "Ctrl R",                 -- a keycap shown on the row
    Callback = function() rejoin() end,
})

Tab:CreateButton({ Name = "Start route", Variant = "Primary", Icon = "zap", Callback = startRoute })

Tab:CreateButton({
    Name = "Delete all waypoints",
    Variant = "Danger",
    HoldToConfirm = true,                -- hold 0.8 s; releasing early cancels
    Callback = function() clearWaypoints() end,
})

local save = Tab:CreateButton({
    Name = "Upload settings",
    Async = true,                        -- runs in a thread; shows a spinner
    Callback = function() return upload() end,  -- return true to flash a check
})
save:SetLoading(true)                    -- busy state from your own code
save:Set("Upload again")                 -- rename (SetTitle)
VariantLook
"Secondary" (default)An action row with an optional leading icon and description, and a chevron or a Shortcut cap on the right.
"Primary"A filled pill with the AccentHi to Accent gradient. Use it for the one main action.
"Ghost"A pill with no fill and AccentText label; the Hover fill appears under the pointer.
"Danger" / "DangerSoft"A solid red pill, or a soft red tint with a red label.

Holds no value, so it takes no Flag. While busy (SetLoading(true) or Async) the button ignores presses. Pair a destructive action with HoldToConfirm or with Window:Confirm.

ButtonGroup new

local controls = Tab:CreateButtonGroup({
    Buttons = {
        { Name = "Pause", Icon = "pause", Callback = pauseRoute },
        { Name = "Stop farming", Icon = "square", Variant = "DangerSoft", Callback = stopFarming },
    },
})

controls.Buttons[1]:SetLoading(true)    -- each entry has SetLoading, Set/SetTitle and SetCallback

Two or three buttons. Each entry takes Name, Callback, Variant (default "Secondary"), Icon and Async.

Choice controls

Dropdown

local target = Tab:CreateDropdown({
    Flag = "Target",
    Name = "Target",
    Options = { "Nearest", "Lowest health", "Random" },
    CurrentOption = { "Nearest" },
    MultipleOptions = false,
    Callback = function(selection) print(selection[1]) end,
})

target:Refresh(fetchTargets())   -- keeps the current selection when it is still valid
target:Set({ "Random" })
print(target.CurrentOption)

Tab:CreateDropdown({
    Flag = "IgnoreTeams",
    Name = "Ignore teams",
    Source = "Teams",                -- or "Players": the list follows the server
    MultipleOptions = true,
    MaxVisibleChips = 2,
})

Options can be strings or tables: { Text = "Highest threat", Description = "Premium", Icon = "flame", Disabled = true }, and { Divider = true } draws a separator. A search field appears automatically above eight options (Searchable = true or false forces it), and long lists only build the rows on screen, so ten thousand options cost the same as ten. Placeholder sets the empty text. Set with an option that does not exist keeps the current selection instead of clearing it. Open(), Close(), IsOpen() and GetSelected() are also available.

Dynamic option lists are safe. A restored value whose option does not exist yet is remembered, then applied the moment Refresh adds that option.

Segmented

Two to four mutually exclusive choices in one strip. The selected segment is marked by its fill, a thin accent outline and its label colour. With more than four options, or when the strip does not fit, it is drawn as a dropdown with the same API.

local mode = Tab:CreateSegmented({
    Flag = "FarmMode",
    Name = "Farm mode",
    Options = { "Safe", "Balanced", "Fast" },
    CurrentOption = "Balanced",
    Callback = function(value) setFarmMode(value) end,
})

mode:Set("Fast")
mode:Refresh({ "Safe", "Fast" })   -- rebuilds the strip without firing the callback
print(mode.CurrentValue)           -- "Fast"

The callback receives a plain string, unlike Dropdown, which passes a table. Left and Right change the focused strip; so does a gamepad's D-pad.

ColorPicker

local esp = Tab:CreateColorPicker({
    Flag = "EspColour",
    Name = "ESP colour",
    Color = Color3.fromRGB(153, 112, 255),
    Alpha = true,                        -- adds an alpha strip
    Callback = function(color, alpha) applyEsp(color, alpha) end,
})

esp:Set("#45C8FF")      -- hex strings and Color3 both work
print(esp.Color, esp:GetHex())

Presets = { Color3, ... } replaces the preset row. The picker keeps its hue when you drag to grey, so moving back out of the grey corner returns to the same colour. On phones it opens as a bottom sheet with the square at full width.

Numeric controls

Slider

Tab:CreateSlider({
    Flag = "FlySpeed",
    Name = "Fly speed",
    Range = { 10, 500 },
    Increment = 5,
    Suffix = "studs",            -- a word gets a space: "60 studs"
    CurrentValue = 60,
    Callback = function(value) CFG.FlySpeed = value end,    -- live while dragging
    OnRelease = function(value) saveRoute(value) end,       -- once, on release
})

Press anywhere on the track to jump and drag. Click the value to type one: Enter commits, Up and Down step, and a value out of range is clamped with a two-second message such as "Must be between 16 and 120". With the keyboard, Left and Right step by Increment, Shift steps by ten, Home and End go to the ends. The mouse wheel never moves a slider, so scrolling the page is safe.

A word suffix gets a space ("10 Bananas"); short suffixes stay attached ("50%", "3ms"). Ticks draws a dot per step: automatic for twelve steps or fewer, true for up to fifty, false for none. Format = function(value) return ... end formats the value pill.

Stepper

Better than a slider when the exact number matters or when dragging on touch is awkward. Values are rounded to Increment and clamped to Range. Hold a button to repeat (it speeds up after a second); a button dims at its limit. Typing works as on the slider.

local charge = Tab:CreateStepper({
    Flag = "ChargeTime",
    Name = "Charge time",
    Range = { 0.05, 2 },
    Increment = 0.05,
    Suffix = "s",
    CurrentValue = 0.45,
    Callback = function(value) CFG.ChargeTime = value end,
})

charge:Set(1.2)

Display and data

ProgressBar

local export = Tab:CreateProgressBar({
    Name = "Export progress",
    Minimum = 0,
    Maximum = 500,
    Format = function(value, maximum) return ("%d / %d"):format(value, maximum) end,
    Thresholds = { { 0.9, "Danger" } },   -- above 90 % the fill turns Danger
})

export:Set(120)
export:Increment(10)
export:SetRange(0, 900)
export:SetColor("Success")      -- a token follows the theme; a Color3 stays fixed
export:SetIndeterminate(true)   -- a sweeping segment while the total is unknown
MemberDescription
Set(value[, silent])Clamps to the range and moves the fill.
Increment(amount)Adds to the current value. Defaults to 1.
SetRange(min, max)Updates the bounds.
SetColor(color)A Color3, or a token name such as "Warning" that follows theme changes.
SetIndeterminate(bool)A segment that moves inside the track, for work with no known total. Indeterminate = true in the config starts that way.
Steps, Thresholds, Format, ShowValueSegments with gaps; recolour points; the readout text; hide the readout.

Progress bars are never saved, so they take no Flag.

List

Rows of records with up to two inline actions and an overflow menu, an optional filter, rename in place and an empty state. Good for saved positions, queues, discovered players or scan results. Long lists only build the rows on screen.

local waypoints = Tab:CreateList({
    Name = "Saved waypoints",
    MaxHeight = 240,
    EmptyText = "No waypoints yet",
    EmptyAction = { Text = "Add current position", Callback = addWaypoint },
    Filter = true,                          -- a filter field above the rows
    Rename = function(record, newName) record.Title = newName; return true end,
    Items = {
        { Title = "Spawn", Description = "0, 12, 0", Actions = {
            { Label = "Teleport", Callback = function(row) teleport(row.Title) end },
            { Label = "Delete", Style = "danger", Callback = function(row) removeWaypoint(row.Title) end },
        } },
    },
})

waypoints:AddItem({ Title = "Shop", Description = "214, 8, -96" })
waypoints:SetItems({ "Default", "Farming" })   -- plain strings become titles
waypoints:Clear()
Item fieldTypeDescription
TitlestringPrimary row text. A plain string item becomes the title.
DescriptionstringA second line; the row grows to fit.
Actionstable{ Label, Callback, Style = "danger" | "warning", Color } per action. The first two show inline (InlineActions changes that); the rest sit behind the ellipsis menu.
Action, Callbackstring, functionThe 1.x single trailing button and the row's own handler (falls back to the list Callback).
ColorColor3Overrides the title colour, handy for status rows.

Other members: RemoveItem, UpdateItem, Select, GetSelected. A danger action asks for confirmation when the list has ConfirmDelete = true. Records keep their identity, so a callback always receives the same table you passed in.

TelemetryCard

local session = Tab:CreateTelemetryCard({
    Title = "Session",
    Content = "Waiting for data…",
    Icon = "activity",
})

session:Set("Uptime 4 m · 128 items")      -- SetContent also works
session:SetStatus("Live", "Success")       -- a token follows the theme; a Color3 stays fixed

Stat new

A big number for live values: a label with an icon, the value in Roboto Mono so it never jitters, a unit, a delta pill and a sparkline of up to twelve bars. Put several in a row to get tiles side by side.

local row = Tab:CreateRow({ Columns = 3 })
local kills = row:CreateStat({ Label = "Eliminations", Icon = "swords", Value = 128, Delta = "+12%", Spark = { 3, 5, 4, 6, 8 } })
local ping = row:CreateStat({ Label = "Ping", Icon = "wifi", Value = 42, Unit = "ms", Status = "Stable" })

kills:SetValue(131)
kills:SetDelta("+14%")
kills:Push(9)                     -- appends a sample to the sparkline (the last 12 are kept)
ping:SetValue(58, "ms")

CreateStat is a TelemetryCard with Variant = "stat". A delta that starts with - is drawn in red with a falling arrow. SetSpark({...}) replaces the whole sparkline.

Callout new

local notice = Tab:CreateCallout({
    Title = "Heads up",
    Content = "Auto farm pauses when the game moves you, for example after a round ends.",
    Type = "warning",                    -- "info", "success", "warning" or "danger"
    Action = { Text = "Learn more", Callback = openGuide },
    Dismissible = true,                  -- or a function, called when dismissed
})

notice:Set({ Type = "success", Content = "Route resumed." })
notice:Dismiss()

Image new

local map = Tab:CreateImage({
    Image = "rbxassetid://1234567890",
    Height = 120,            -- default
    ScaleType = "Fit",       -- default; any Enum.ScaleType name
    Corner = 10,             -- default corner radius
    Caption = "Map of the arena, updated each round.",
})

map:Set({ Image = "rbxassetid://987654321", Caption = "Round 2" })

Label and Paragraph

Tab:CreateLabel("Autosave runs a moment after each change.", "info")
Tab:CreateLabel({ Text = "Running on a private server", Icon = "lock", Style = "warning" })

local details = Tab:CreateParagraph({
    Title = "How targeting works",
    Content = "Nearest uses straight-line distance and ignores players behind walls.",
})
details:Set({ Title = "Targeting", Content = "Updated text." })   -- or Set("text") for the body

CreateLabel(text, icon, color) follows Rayfield's signature. Style is "muted", "info", "success", "warning" or "danger"; it tints the icon, and the text too for danger. Labels and paragraphs size themselves to their text.

Layout

local advanced = Tab:CreateDivider("Advanced")   -- the caption is optional
advanced:Set("Danger zone")                      -- new caption; Set(false) hides it
Tab:CreateSpace(16)                              -- 4 to 80 pixels

Command palette new

Press Ctrl K, press / with the pointer over the window, click Search in the sidebar, or press Y on a gamepad. Type a few letters of any control in any tab, including tabs that were never opened.

KeyWhat it does
↑ ↓Move through the results.
EnterGo to the control: switch tab (building it if needed), scroll it into view and highlight the row. Commands run.
Ctrl EnterRun in place: flip a switch, press a button, cycle a segmented control. The palette stays open and the value chip updates.
← →Adjust a slider, stepper, segmented control or switch result without leaving the palette (Shift for ×10).
Backspace on an empty field, pad BClose the palette.

Ranking prefers a word that starts with what you typed, then text that contains it, then a loose match of the letters in order. Names count most, then Keywords, option names, and descriptions. Recently used results come first when the field is empty.

Tab:CreateToggle({ Name = "Box ESP", Keywords = { "esp", "wallhack", "boxes" } })

local command = Window:RegisterCommand({
    Name = "Rejoin server",
    Description = "Reconnects to this place",
    Icon = "refresh-cw",
    Keywords = { "reconnect", "server hop" },
    Shortcut = "Ctrl R",                 -- shown as keycaps in the result
    Callback = function() rejoin() end,
})
command.Remove()                         -- take it out again

Window:OpenPalette("speed")              -- open it from code, already filled in

Built-in commands: Switch theme, Toggle material, Reduce motion, Show keybind list, Toggle watermark, Save profile as, Open settings, Hide UI and Unload. Palette = false in CreateWindow turns the palette off.

Row menu new

Right-click any row, hold it for 450 ms on touch, press Shift F10 on the focused row, or press X on a gamepad. The menu opens beside the value it acts on, not over it.

local fov = Tab:CreateSlider({ Flag = "Combat.FOV", Name = "Field of view", Range = { 30, 180 }, CurrentValue = 90 })

fov:AddMenuItem({ Text = "Explain this setting", Icon = "info", Callback = function()
    Window:Dialog({ Title = "Field of view", Content = "The radius of the aim circle, in degrees." })
end })

Tab:CreateButton({ Name = "Rejoin", ContextMenu = false, Callback = rejoin })   -- no menu on this row

Copy and paste work between controls of the same kind through the library's own clipboard, so a slider value can be pasted into another slider. Copying also tries the executor's setclipboard; without it, a sheet shows the text so it can be copied by hand. A paste can be undone like a reset.

Changes and undo new

Every control knows its default: the value it was created with, or Default in its config. A changed row gets a small dot after its title (click the dot to reset that row). The page header says "3 changed · Reset tab", a section with changes shows "Reset section" in its header, and every reset or paste can be undone for six seconds with the Undo pill or Ctrl Z.

print(walkSpeed.Default, walkSpeed:IsChanged())
walkSpeed:Reset()                 -- back to the default (fires the callback)

print(Tab:GetChangedCount())
Tab:Reset()                       -- every changed control in the tab, one Undo
Section:Reset()
Window:ResetAll()                 -- every tab

Resetting a tab from its header chip asks for confirmation when more than three values would change. Undo keeps one level: the last reset or paste, for six seconds.

Pins and quick dock new

Switches and buttons can be pinned from the row menu, or by you with Pinned = true. Pinned controls get a Pinned group in the sidebar and a quick dock: labelled chips next to the dock pill or reopen orb that stay on screen while the window is hidden, so the few toggles you use mid-fight are one tap away. Both are bound to the same state as the real control, so they can never disagree.

Tab:CreateToggle({ Flag = "Fly", Name = "Fly", Keybind = "F", Pinned = true })
Tab:CreateButton({ Name = "Rejoin server", Pinned = true, Callback = rejoin })

Up to six pins on PC and four on touch; pinning a seventh says "Unpin one first". Pins are saved per profile. Every chip carries a text label, never an icon alone.

Keyboard and gamepad

Every interaction works with a mouse, a keyboard, touch and a gamepad. Press an arrow key or Tab while the pointer is over the window, or use a gamepad, and focus mode starts: a focus ring moves from row to row in reading order, and the focused row's control takes Left, Right, Space and Enter. Moving the mouse away ends it.

KeyboardGamepadWhat it does
Insert (or your ToggleUIKeybind)Hide or show the window
Ctrl K, /YCommand palette
Ctrl 1 to 9, Ctrl [ ]LB / RBSwitch tabs
↑ ↓, TabD-pad ↑ ↓Move the focus ring
← → (Shift for ×10), Home EndD-pad ← →Adjust the focused control
Space, EnterAActivate: flip, press, open the picker, start key capture
Shift F10XRow menu
Ctrl ZUndo the last reset or paste while its Undo pill is showing
BackspaceBClose the top popup, menu, sheet or dialog, then leave focus mode
Escape is left alone on purpose. In Roblox, Escape opens the game menu, so the library never uses it: Backspace and gamepad B close the top layer, and Backspace or Delete clear a keybind during capture. Backspace and B are only taken while a closable layer is open, so a tool is not dropped by accident, and / and Ctrl 1-9 only act while the pointer is over the window or focus mode is on.

Status and badges new

Window:SetStatus("Auto farm running", "success")   -- "success", "warning", "danger", "info" or "neutral"
Window:SetStatus()                                 -- no text hides the chip

Automation:SetBadge(3)        -- a count
Visuals:SetBadge("New")
Players:SetBadge(true)        -- a dot
Players:SetBadge(nil)         -- none

The status chip replaces 1.x's fixed "LIVE" pill: it shows only what you set. Tab groups come from CreateTab(name, icon, { Group = "Tools" }) and appear as captions in the sidebar; tabs without a group sit under no caption.

Element methods

Every control returned by a Create* call shares this surface. Create* only records the control; its instances are built later, a few rows per frame. Reading an instance field such as element.Root builds that control on the spot, so old scripts that reach into instances keep working.

MemberDescription
Set(value[, silent])Updates the value. true as the second argument skips the callback and autosave. A script's Set works even while the control is locked or disabled.
Get() · CurrentValueThe current value. Dropdowns also expose CurrentOption; keybinds CurrentKeybind.
Default · GetDefault()The value Reset returns to.
IsChanged() · Reset()Whether the value differs from the default; put it back.
OnChanged(fn)Calls fn(value, source) after every change. Returns a connection.
SetCallback(fn)Replaces the callback.
Type · FlagThe control kind, for example "Toggle", and its configuration key when one was given.
SetTitle(text) · SetDesc(text) · SetIcon(name)Retitles the row, sets its description line, changes its icon.
SetTooltip(text)Hover text that costs no row height. Tooltip is an alias, and element.HasTooltip tells whether one is set.
Lock(reason) · Unlock() · SetLocked(locked, reason)Blocks every user path, including the control's keybind, and shows a lock chip with the reason.
SetDisabled(bool) · IsDisabled()Greys the row out without a reason; focus skips it.
SetVisible(bool)Shows or hides the row. A hidden row stays hidden across tab switches and searches.
AddMenuItem({ Text, Icon, Callback })Adds an item to the row menu.
Focus() · Flash()Moves the focus ring to the row; flashes it.
Destroy()Removes the control. Its flag is released (the last value is kept for the next save) and its keybinds are freed.
local sync = Tab:CreateToggle({ Name = "Cloud sync", Tooltip = "Available in the paid tier." })
sync:SetDesc("Requires an account")
sync:Lock("Requires Premium")
sync:Unlock()

Tab:CreateSlider({
    Name = "Chams opacity",
    Range = { 5, 100 }, CurrentValue = 60,
    VisibleWhen = { Flag = "Chams", Equals = true },   -- shown only while Chams is on
})

Tooltips

A tooltip costs no row height, which is why the mark exists: without it nothing would say that hovering explains more. It opens after 400 ms on hover, after 600 ms when the row has keyboard focus, and on a tap of the mark on touch; moving straight from one tooltip to the next opens it at once. Put anything a user must read before acting in Description instead.

Locks

A lock states the precondition up front instead of letting the click fail. Use it for dependent settings and for pairs that cannot run together. Keyboard focus skips locked rows and the gamepad hint bar names the reason.

Configuration and persistence

Give a control a Flag and it becomes part of the saved configuration. Values are written to FolderName/FileName shortly after a change, and every write is read back to confirm it landed. 1.x configuration files load unchanged.

local Window = Argon:CreateWindow({
    Name = "My Hub",
    ConfigurationSaving = {
        Enabled = true,
        FolderName = "MyHub",
        FileName = "config-" .. tostring(game.Players.LocalPlayer.UserId),
    },
})

-- build every tab and control first, then:
local ok, info = Argon:LoadConfiguration()
if not ok then warn("config:", info) end

How loading behaves

Callbacks run once

A restored value runs its callback once, even when it equals the control default, so a feature cannot silently stay off.

Autosave turns itself on

Persistence starts after loading, or a few seconds after CreateWindow if you never load.

Unknown flags survive

Values for controls that do not exist yet are kept, applied when that flag appears, and never dropped on save.

Writes are verified

Saves read the file back and return false, reason when the contents do not match.

LoadConfiguration() returns true (with an info string) when there is no saved file yet, and when every value is waiting for a control that does not exist yet. Colour values in a file without the __argonColor marker are read as 0-255, which is Rayfield's format. Window preferences (theme, accent, scale, size and position, overlays, pins) are kept in a small .meta.json file next to the configuration, or in ArgonUI/<GuiName>.meta.json when saving is off, so they survive even without ConfigurationSaving.

Working with values directly

print(Argon:GetFlagValue("AutoFarm"))
Argon:SetFlagValue("FlySpeed", 220)      -- applies through the control and saves
local everything = Argon:GetFlags()       -- flag -> value snapshot

local unbind = Argon:OnFlagChanged(function(flag, value)
    print("changed:", flag, value)
end)
unbind()                                  -- removes only this listener

local stats = Argon:GetConfigurationStats()
print(stats.Path, stats.Ready, stats.PendingFlags, stats.LastError)

Resetting

Argon:SaveConfiguration()     -- returns ok, path
Argon:DeleteConfiguration()   -- removes the file
Argon:ResetConfiguration()    -- clears stored values and deletes the file
Several accounts. Put the identifier in FileName, or use profiles. Separate files never overwrite each other.

Profiles new

Profiles are named configurations stored next to the main file. The default file is the profile called Default.

Argon:SaveConfiguration("PvP")             -- save the current values as a profile
Argon:LoadConfiguration("PvP")
Argon:ListConfigurations()                 -- { { Name, Path, Autoload, Count, SavedAt, Active }, ... }
Argon:RenameConfiguration("PvP", "Arena")
Argon:DeleteConfiguration("Arena")

Argon:SetAutoloadConfiguration("Grinding") -- LoadConfiguration() with no name loads it from now on
print(Argon:GetAutoloadConfiguration())

local code = Argon:ExportConfiguration("Grinding")   -- an "ARGON2:..." string to share
local ok, summary = Argon:ImportConfiguration(code)  -- applies it; summary = { changed, unknown, invalid }

Window:CreateConfigManager(Tab)            -- a ready-made manager with all of the above

ImportConfiguration also accepts a raw JSON configuration, such as a 1.x or Rayfield file. Pass { apply = false } to only count what would change, or { saveAs = "Name" } to store it as a profile. In the manager, Delete asks for confirmation, and Import shows how many settings would change before it applies anything.

Themes

Twelve built-in themes: the ten from 1.x with their accents kept, plus the light Daylight and HighContrast (also accepted as "High contrast"). Every theme meets fixed contrast targets: primary text at least 10:1 on platters, secondary text at least 9:1. Theme changes apply live and are saved.

ThemeKindFrameSurfaceTextAccentAccentHi
Argondark#0C0E16#1F212A#F1F3F9#9970FF#C3AAFF
Amethystdark#100B1A#241E2F#F5F0FF#A76DFF#D2B2FF
Midnightdark#080C15#1B202B#EEF4FF#58A6FF#9BCDFF
Emeralddark#080F0E#1B2322#EEF9F6#34D399#97F6D3
Rosedark#130A10#281D24#FFEFF8#F472B6#FBB6DC
Slatedark#0D1014#1F2329#EEF3F9#7DC4FF#BADEFF
Emberdark#120B08#261E1B#FFF1E9#FF8A4C#FFBE96
Norddark#1E222B#2F343F#ECEFF4#88C0D0#BEE0E8
Monodark#0C0C0E#202022#F4F4F6#E2E2E8#FFFFFF
Oceandark#071018#1A242C#E9F6FC#38BDD0#96E7F0
Daylightlight#E5E7EF#FFFFFF#12141C#6B4AE6#7C5DED
HighContrasthigh contrast#000000#111111#FFFFFF#FFE14D#FFF08A
print(Argon:GetThemes())
Window:SetTheme("Midnight")              -- applied live and remembered

Argon:AddTheme("Mine", {
    Frame   = Color3.fromRGB(9, 11, 14),
    Surface = Color3.fromRGB(24, 28, 35),
    Accent  = Color3.fromRGB(120, 190, 255),
})                                        -- every missing token is derived
Argon:SetTheme("Mine")

Argon:CreateTheme({                       -- a complete theme from one colour
    Name = "Coral",
    Accent = Color3.fromRGB(255, 111, 97),
    Mode = "Dark",                        -- or "Light"
    Neutral = "warm",                     -- "cool", "warm" or "neutral"
})

A palette may set any subset of the tokens below. Missing ones are derived from the ones you gave, and adjusted only as far as needed to meet the contrast targets; a key you set is never changed. SetTheme(table) registers the table as the theme Custom and it is remembered across sessions. Rayfield's theme names (Default, AmberGlow, Bloom, DarkBlue, Green, Serenity and Light) are accepted too.

Tokens

2.0 names its colours by role. The frame is the only see-through surface; text-dense content sits on Sheet and Surface, which are solid.

Token1.x nameUsed for
FrameBackground, SidebarThe glass window frame, the sidebar and the icon rail
SheetWindowThe opaque content sheet: topbar, page and "On this page"
SurfaceSurfaceSection platters, stat tiles, list bodies
HoverHoverRow hover and menu rows
ActiveActivePressed and selected fills, Secondary buttons
Raised(new)Floating layers: popovers, menus, toasts, dialogs, the palette, the loader card
WellInputRecessed fields: inputs, dropdown triggers, value pills, segmented and stepper tracks
Track(new)Switch-off track, slider groove, progress track
BorderBorderControl edges
BorderSoftBorderSoftDividers
TextTextPrimary text
Text2(new)Secondary text; the only text colour besides Text used on the glass frame
MutedMutedDescriptions and meta text
DimDimPlaceholders and hints
Disabled(new)Disabled text and glyphs
AccentAccentBrand and state colour
AccentHiAccentLightLight end of the active gradient; the focus ring in dark themes
AccentDeepAccentDarkPressed accent, start of the slider fill
AccentAlt(new)Second hue, used only by progress and loader bars
AccentText(new)Accent as text: links, changed dots, the selected segment
OnAccent(new)Ink on accent fills (primary button label, checkbox check)
AccentSoft(new)Solid accent tint: selected segment, listening keycap, badges
Focus(new)Focus ring and focused field stroke
Success, Warning, Danger, InfoSuccess, Warning, DangerStatus colours (Info is new)
SuccessSoft, WarningSoft, DangerSoft, InfoSoft(new)Solid status tints for callouts, delta pills and toast icon plates
OnStatus(new)Ink on solid status fills
1.x names still work. Background, Window, Sidebar, Input, AccentLight and AccentDark are aliases of Frame, Sheet, Frame, Well, AccentHi and AccentDeep. They can be read with GetColor and in OnThemeChanged palettes, and written in AddTheme and SetTheme(table).

Controls the library builds re-theme themselves. Frames your own script builds sit outside the library, so read the live palette instead of baking colours in:

myFrame.BackgroundColor3 = Argon:GetColor("Surface")

local unbind = Argon:OnThemeChanged(function(palette, themeName)
    myFrame.BackgroundColor3 = palette.Surface
end)

Argon:GetColor(key, fallback) resolves one token, Argon:GetPalette() returns a detached copy, and Argon.Palette is the live table. Right after a theme switch, an instance in a hidden tab can still hold the old colour until that tab is shown, so read colours from GetColor or GetPalette rather than from library instances.

The topbar picker

The palette icon in the topbar opens the Appearance popover: theme cards drawn in each theme's real colours (hovering a card previews it on the window, a click keeps it), the accent row, the material, interface scale, density, state labels and reduced motion, and a link to the Settings tab. On phones it opens as a bottom sheet.

local Window = Argon:CreateWindow({ Name = "My Hub", ThemePicker = false })   -- no topbar button

Window:SetThemePickerVisible(true)   -- bring it back
Window:ToggleThemePicker()           -- open or close it from code

Appearance

Users change these in the Appearance popover and the Settings tab; you can set them from code. All of them are saved and applied before the first instance on the next run.

Argon:SetAccent(Color3.fromRGB(255, 120, 60))   -- your accent on any theme
Argon:SetAccent(nil)                            -- back to the theme's own accent

Window:SetMaterial("Clear")       -- "Solid", "Tinted" (default) or "Clear"
Window:SetScale(1.1)              -- 0.75 to 1.5, or "auto"
Window:SetDensity("compact")      -- "auto", "compact" or "touch"
Window:SetReducedMotion(true)     -- Argon:SetReducedMotion(true) also works
Window:SetFont("BuilderSans")     -- see the typeface sets below
MaterialFrameSheetDock and orbOverlays
Solidopaqueopaqueopaqueopaque
Tinted (default)14 % see-throughopaque6 %10 %
Clear25 % see-through6 %12 %18 %

Platters, popovers and fields are always opaque. Daylight defaults to Solid, and High contrast always uses Solid. Accent changes re-derive the related tokens (AccentHi, AccentDeep, AccentText, OnAccent, AccentSoft, Focus) so text on the accent keeps its contrast.

DensityRow heightControl heightSwitch
compact36 px (48 with a description)28 px40 × 22
default (mouse)44 px (56)32 px44 × 24
touch52 px (64)40 px52 × 30

"auto" follows the input device: touch gets the touch sizes, a gamepad gets slightly larger targets and the hint bar. Reduced motion makes springs instant, caps fades at 100 ms and stops the loader's loops; SetSmoothTransitions(false) turns it on too.

Typefaces

The default set is Roboto: Roboto for rows and body text and Montserrat SemiBold for page, section and dialog titles, with Roboto Mono for values. The loader and the first frame use only faces that ship with the Roblox client; Roboto Medium and Montserrat SemiBold are loaded in the background and swapped in, so text never waits on a download. Argon:GetFonts() lists the sets.

SetBodyTitles
Roboto (default)RobotoMontserrat
BuilderSansBuilderSansBuilderSans
UbuntuUbuntuUbuntu
CondensedRobotoCondensedOswald
TechnicalSarpanchSarpanch
GothamGothamSSmGothamSSm

Text is shown as you wrote it: section headers, the subtitle and loader status are never turned into capitals.

Overlays, dialogs and the settings tab

Overlays

A player list, a keybind list and a watermark (fps, ping and time). All three are off unless you turn them on. They are draggable, remember their positions, and can stay on screen while the window is hidden. The keybind list shows every bind, highlights the active ones, and hides itself when there are none.

Argon:CreateWindow({ Name = "My Hub", PlayerList = true, KeybindList = true, Watermark = true })

Window:SetWatermarkVisible(true)
Window:TogglePlayerList()
Window:SetKeybindListVisible(false)
Window:SetOverlaysAlwaysVisible(true)   -- keep them while the window is hidden
Window:SetOverlayTransparency(0.3)      -- the 1.x range 0.12-0.8, mapped onto the overlay material
Window:SetOverlayScale(1.1)
Window:ResetOverlayPositions()

Dialogs

Window:Confirm({
    Title = "Delete profile?",
    Content = "\"Farming\" and its saved settings will be removed.",
    ConfirmText = "Delete",
    OnConfirm = function() deleteProfile("Farming") end,
    OnCancel = function() print("kept") end,
})

Window:Prompt({
    Title = "Rename profile",
    Placeholder = "Profile name",
    Default = "Farming",
    Validate = function(text)
        if #text < 2 then return false, "Use at least 2 characters" end
        return true
    end,
    OnSubmit = function(text) renameProfile("Farming", text) end,
})

Window:Dialog({
    Title = "Server full",
    Content = "Try again in a minute.",
    Buttons = {
        { Title = "Later" },
        { Title = "Retry", Variant = "Primary", Callback = function() retry() end },
    },
})

Enter activates the focused button, and the Enter cap is drawn on that button. A destructive Confirm (the default Variant = "Danger") focuses Cancel first. The x, Backspace, gamepad B and a click outside the dialog all cancel and call OnCancel; DismissOnOutside = false keeps a click outside from closing it. In Prompt, Validate runs as the user types and returns false, "message" to reject the value; Save stays disabled until it passes. Popup is an alias of Dialog.

Built-in settings tab

One call adds a complete settings page for your users. Its rows are views over the window preferences rather than flags, so they never collide with yours; the 1.x ArgonUI* flag names still work through GetFlagValue and SetFlagValue. The account menu and the palette can open a settings page even if you never create one.

local Settings = Window:CreateTab("Settings", "settings")
Window:CreateLibrarySettings(Settings)
SectionRows
AppearanceTheme, material, interface scale, density, state labels, reduce motion, typeface
WindowMenu name and subtitle, custom logo
ControlsMenu key, reopen button, mouse unlock
NotificationsShow callback errors, clear inbox
OverlaysKeybind list, player list, watermark, keep overlays on screen, opacity, scale, overlay hotkeys, reset positions
ProfilesThe profile manager
AboutCopy diagnostics, Unload

Notifications

Argon:Notify({ Title = "Saved", Content = "Configuration written.", Type = "success" })

local job = Argon:Notify({
    Id = "download",
    Title = "Downloading map",
    Type = "loading",                     -- a spinner and no timer
    Actions = { { Text = "Cancel", Callback = function() cancelDownload() end } },
})
job:SetProgress(0.4)
job:Update({ Title = "Map ready", Type = "success" })
-- job:Dismiss()

Argon:Toast("Copied", "success")          -- a small notice
Argon:Toast("Reset 3 settings", "info", { Text = "Undo", Callback = undoReset })
OptionDescription
Title, ContentThe text. Description also works for the body.
Type"info", "success", "warning", "error" or "loading". Without it the type is guessed from whole words in the title, so "Save failed" reads as an error and "No errors found" does not.
Icon / ImageReplaces the type's icon.
DurationSeconds. By default it follows the text length (3 to 9 s), errors last 1.5 times longer, and a card with actions stays at least 8 s. math.huge keeps it until dismissed.
ActionsUp to two buttons: { Text, Callback }. A button dismisses the card unless it has Keep = true.
IdNotifying again with the same Id updates that card in place.

Notify returns a handle with Update, Dismiss and SetProgress, and it works before CreateWindow. Cards are not tied to the window, so a background feature can report while the menu is hidden. Identical cards within two seconds merge into one with a count, at most four are on screen with "+N more in inbox" for the rest, and everything also lands in the inbox behind the bell in the topbar. The stack never covers the window's topbar.

Errors in your callbacks are shown, not swallowed. An error inside any callback becomes an error card with Copy error and Retry (Retry runs the callback again with the same value) instead of a warning in the console. Users can turn this off in the Settings tab.

Library API

The table that loadstring(game:HttpGet(url))() returns. Loading it creates no instances, does no file or network work and starts no timers.

Windows and notifications

Argon:CreateWindow(options) → window

Creates a window and returns at once. See Windows for the options. Creating a second window with the same GuiName replaces the first.

Argon:SetVisibility(visible) · Argon:IsVisible() → boolean

Shows or hides the last window created.

Argon:Notify(options) → handle

Shows a notification card. Title, Content, Type, Icon, Duration, Actions, Id. The handle has Update(patch), Dismiss() and SetProgress(fraction). Works before CreateWindow. See Notifications.

Argon:Toast(text, type?, action?) → handle

A short notice. action is one { Text, Callback } button.

Configuration and profiles

Argon:LoadConfiguration(name?) → ok, info

Restores values, runs each restored callback once and turns autosave on. With a name, loads that profile; without one, loads the autoload profile or the default file. Returns a warning string when some flags failed or are unknown.

Argon:SaveConfiguration(name?) → ok, pathOrReason

Writes and verifies now. With a name, saves the current values as that profile.

Argon:DeleteConfiguration(name?) · Argon:ResetConfiguration()

Deletes a profile or the default file; clears stored values and deletes the file while cancelling a queued autosave.

Argon:ListConfigurations() · RenameConfiguration(old, new) · SetAutoloadConfiguration(name) · GetAutoloadConfiguration()

Profile management. Each list entry has Name, Path, Autoload, Count, SavedAt and Active.

Argon:ExportConfiguration(name?) → string · Argon:ImportConfiguration(text, opts?) → ok, summary

An ARGON2: code to share, and its reverse. Import also accepts raw JSON and applies the values unless opts.apply is false; summary counts changed, unknown and invalid.

Argon:GetFlagValue(flag) · Argon:SetFlagValue(flag, value) → ok, reason · Argon:GetFlags()

Reads a live value (or a stored one whose control is not built yet), applies a value through its control, and snapshots every flag.

Argon:OnFlagChanged(fn) → unbind

Calls fn(flag, value, element) on user edits and restores. The returned function removes only this listener.

Argon:GetConfigurationStats() · Argon:SetPersistenceReady(value[, saveNow])

Path, Enabled, Ready, Loading, RegisteredFlags, PendingFlags, LastError, LastSaveAt and more; and the manual autosave gate, still useful to hold writes during a long startup.

Themes, fonts and motion

Argon:SetTheme(nameOrTable) → ok, err · Argon:GetThemes()

Applies a built-in theme, a Rayfield theme name or a palette table to every live control.

Argon:AddTheme(name, palette) · Argon:CreateTheme(spec) → palette

Registers a palette (missing tokens are derived), or generates a complete one from { Name, Accent, Mode, Neutral, Contrast }.

Argon:SetAccent(color3 | nil)

Uses your accent on any theme and re-derives the related tokens. nil restores the theme's own accent.

Argon:GetColor(key, fallback?) · Argon:GetPalette() · Argon:OnThemeChanged(fn) → unbind

One token (2.0 or 1.x name), a detached copy of the palette, and fn(palette, themeName) after every theme change. Argon.Palette is the live table.

Argon:SetFont(name) · Argon:GetFonts() · Argon:GetFontLabel(name)

Switches the typeface set live. See Typefaces.

Argon:SetReducedMotion(on)

Instant springs, fades capped at 100 ms, no loops.

Icons, diagnostics and teardown

Argon:GetIcon(name) → { Image, ImageRectOffset, ImageRectSize } · Argon:HasIcon(name)

Resolves a Lucide name for your own frames. See Icons.

Argon:GetPerfStats() → table

What the library is doing right now. See Performance.

Argon:PruneRegistries() → counts

Kept for 1.x scripts. There are no global registries left, so it returns zero counts.

Argon:Destroy()

Saves, disconnects everything and removes every window.

Argon.Version · Argon.Flags · Argon.Palette · Argon.Themes · Argon.CurrentTheme · Argon.CurrentFont

Flags maps flag names to control objects, which keeps profile helpers simple. Version is "2.0.0" for this release.

Window API

MethodDescription
CreateTab(name, icon, options?)Adds a tab. options takes Group and Description.
SelectTab(tab | name | index) · GetActiveTab()Switches tabs; returns the active one.
Open() · Close() · Toggle() · SetVisibility(bool) · IsVisible()Visibility. ToggleVisibility() also exists.
SetToggleKey(key)Changes the hide and show key; "None" removes it.
SetOpenButton(mode) · SetMouseUnlock(bool)The dock pill or orb ("auto", "always", "off"); freeing the cursor while open.
SetTitle(text) · SetSubtitle(text) · SetLogo(asset)Branding, remembered across sessions. GetTitle() and GetSubtitle() read it.
SetStatus(text?, tone?)The topbar status chip. See Status and badges.
SetTheme(name) · GetThemes() · SetFont(name) · GetFonts()Theme and typeface, saved as preferences.
SetMaterial(name) · SetScale(n) · GetScale() · SetDensity(name) · SetReducedMotion(bool)Appearance. See Appearance.
SetSmoothTransitions(bool)false turns on reduced motion.
SetSize(value)A Vector2 or { width, height }, clamped to the screen and the 560×400 minimum.
SetLoadingProgress(fraction, status) · SetLoadingStatus(text) · FinishLoading() · IsLoading()The loader. See Loading.
OnLoaded(fn) · Build(fn)Runs after the window opens; runs your builder with errors shown on the loader.
OpenPalette(query?) · RegisterCommand(command)The command palette.
Dialog(config) · Confirm(config) · Prompt(config)Dialogs. Popup is an alias of Dialog.
ResetAll()Resets every changed control in every tab, with one Undo.
CreateLibrarySettings(tab) · CreateConfigManager(tab)The built-in settings page; the profile manager on its own.
SetThemePickerVisible(bool) · ToggleThemePicker()The Appearance button and popover.
SetPlayerListVisible(bool) · TogglePlayerList() · SetKeybindListVisible(bool) · ToggleKeybindList() · SetWatermarkVisible(bool)Overlays.
SetOverlaysAlwaysVisible(bool) · ToggleOverlaysAlwaysVisible() · AreOverlaysAlwaysVisible()Keep the overlays while the window is hidden. SetOverlaysPinned is an alias.
SetOverlayTransparency(n) · SetOverlayScale(n) · ResetOverlayPositions()Overlay appearance.
SetCursorVisible(value?) · IsCursorDrawn()The drawn cursor. See Cursor.
Destroy()Removes this window, its keybinds and its layers.
ScreenGui · Tabs · ActiveTabUseful properties for advanced hosts. Instance fields such as Main are built when first read.

Icons

All 1,573 Lucide icons are built into the file as an 18 KB table. Nothing is fetched, so icons draw on the first frame, offline too. An unknown name draws a dashed circle (and GetIcon returns nil for it), and a Roblox asset id ("rbxassetid://..." or a number) works anywhere an icon name does.

Window:CreateTab("Mining", "pickaxe")
Window:CreateTab("Combat", "swords")
Window:CreateTab("Alerts", "bell-ring")

-- Resolve an icon yourself for your own frames.
local icon = Argon:GetIcon("shield-check")
if icon then
    imageLabel.Image = icon.Image
    imageLabel.ImageRectOffset = icon.ImageRectOffset
    imageLabel.ImageRectSize = icon.ImageRectSize
end
print(Argon:HasIcon("rocket"))

Performance

2.0 was rebuilt for hubs with hundreds of controls. What that means in practice:

local stats = Argon:GetPerfStats()
print(stats.mountedElements, stats.pendingElements, stats.instancesCreated)
FieldMeaning
elements · mountedElements · pendingElementsControls created, built, and still waiting to be built.
instancesCreatedInstances the library has created so far.
liveScopesLive ownership scopes (each control, tab and layer owns one).
globalHandlers{ InputBegan, InputChanged, InputEnded } handler counts.
springsAndLoops · dragSessions · popoversOpenAnimations running, drags in progress, open popovers. All zero at idle.
schedulerThe build scheduler's queue and timing.
iconsDecoded · bundleVersionIcons decoded so far; the library version.

Measured numbers against 1.x are in bench/RESULTS.md. The Settings tab's Copy diagnostics copies these stats for bug reports.

Recipes

Progress-reporting task

local bar = Tab:CreateProgressBar({ Name = "Scan", Maximum = #targets })
local results = Tab:CreateList({ Name = "Results", MaxHeight = 200, EmptyText = "Nothing found yet" })

Tab:CreateButton({ Name = "Start scan", Variant = "Primary", Icon = "search", Callback = function()
    local job = Argon:Notify({ Id = "scan", Title = "Scanning", Type = "loading" })
    task.spawn(function()
        for index, target in ipairs(targets) do
            bar:Set(index)
            job:SetProgress(index / #targets)
            if inspect(target) then results:AddItem({ Title = target.Name, Description = "match" }) end
            task.wait()
        end
        job:Update({ Title = "Scan complete", Content = ("%d targets checked"):format(#targets), Type = "success" })
    end)
end })

Mirror settings into your own config table

local CFG = { FlySpeed = 60, FarmMode = "Balanced" }

Argon:OnFlagChanged(function(flag, value)
    if CFG[flag] ~= nil then CFG[flag] = value end
end)

Show persistence health in your UI

task.spawn(function()
    while true do
        local stats = Argon:GetConfigurationStats()
        if stats.LastError then
            Window:SetStatus("Save failed", "danger")
        elseif stats.Ready then
            Window:SetStatus(("%d flags saved"):format(stats.RegisteredFlags), "success")
        else
            Window:SetStatus("Not saving yet", "neutral")
        end
        task.wait(2)
    end
end)

Build safely

local Window = Argon:CreateWindow({ Name = "My Hub" })

Window:Build(function(window)
    local Main = window:CreateTab("Main", "zap")
    buildMain(Main)       -- if this errors, the loader shows the error with Open anyway and Copy error
end)

Argon:LoadConfiguration()

Migrating from Rayfield-style scripts

The window, tab, control, flag, notification and configuration calls keep Rayfield's shapes, so most menus only need their loader swapped.

-- before
local Rayfield = loadstring(game:HttpGet("https://sirius.menu/rayfield"))()

-- after: the rest of the script can stay as it is
local Rayfield = loadstring(game:HttpGet(
    "https://raw.githubusercontent.com/JirkaachS/argon-ui/main/argon-ui.luau"
))()
RayfieldIn Argon UI
LoadingTitle, LoadingSubtitleShown on the loader card when one appears (LoadingScreen controls it).
Icon on the windowBecomes the Logo; a number is treated as an asset id.
KeySystem, KeySettingsSupported, plus Validate and GetKeyLink.
Theme namesDefault, AmberGlow, Bloom, DarkBlue, Green, Serenity and Light map to generated or built-in themes.
Keybind CallbackFires on press, as in Rayfield; HoldToInteract passes true and false.
CreateLabel(text, icon, color)The same signature.
Saved coloursFiles without the __argonColor marker are read as 0-255, Rayfield's format.
ScreenGui nameArgonUI by default; override with GuiName.
Config file extensionFileName without an extension gets .rfld; an extension you give is kept.
Restored callbacksAlways fire once on load, even when the value matches the default.
Save resultsSaveConfiguration returns ok, path or false, reason instead of only a boolean.
SectionsOptional; a control created before any section goes into General.

Tests

The repository runs the real library outside Roblox against a Roblox API sandbox, so control logic, persistence and the loader are checked without launching a game.

# requires https://github.com/lune-org/lune
lune run tests/run_all.luau

Changelog

2.0.0 (2026-11-01)

Argon UI 2 is a rebuild. The public API is kept source-compatible with 1.18: every method, alias, option key, callback payload and saved-value format still works, and 1.x configuration files load unchanged. Underneath, the library is now about 80 small modules in src/, bundled by build.luau into the same single argon-ui.luau, so loadstring(game:HttpGet(url))() keeps working.

The design behind it is in design/v2/.

Faster loading

An honest loader

A new look

New features

Changes you might notice when upgrading from 1.x

The full list of 25 behaviour changes is under Upgrading from 1.x.

Tests

1.18.3

The last 1.x release. Earlier changes are in the repository's git history and in CHANGELOG.md.

Argon UI 2.0.0 is a standalone Roblox Luau UI library. Source, issues and the changelog are on GitHub; the browser preview runs the interface in this page's browser.