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.
Load it and build a window
One loadstring, a window, a tab, a control. About twenty lines to a working menu that saves itself.
See every control
Drawings of the real rows next to the calls that make them, so you can pick a control by sight.
What it looks like → 3 · UpgradingComing from 1.x
Replace the file and your script keeps working. Twenty-five small behaviour changes are listed in one place.
Upgrading from 1.x → 4 · New in 2.0Find anything with Ctrl+K
The command palette finds any control in any tab, including tabs that were never opened, and changes it in place.
Command palette →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()
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()
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:
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.
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.
LookGlass 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.
FindCommand 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.
Change tracking and undo
Changed rows get a dot, tabs and sections reset, and Ctrl+Z or the Undo pill brings the values back.
MenuRow 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.
ProfilesNamed configurations
Save, load, rename and delete profiles, pick one to load on start, and share them as an ARGON2: code.
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.
PinsPins 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.
InputKeyboard 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.
ControlsNew blocks
Callout, Stat with a sparkline, Image, ButtonGroup, Checkbox, button variants and Keybind = "F" on toggles and buttons.
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:
- 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.
- 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. - A Keybind whose config has
Callbackbut noOnPressed/Pressednow callsCallback()on press, as Rayfield does (withHoldToInteract,Callback(true)on press andCallback(false)on release). Rebinds fireOnChanged(key). Scripts that usedCallbackas a rebind listener should addCallOnChange = trueor useOnChanged. Configs withOnPressedkeep the 1.x behaviour. - 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.
- A notification's
Typeoverrides the guess from its title, and the guess now matches whole words ("No errors found" is not an error). - 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:SetStatusadds a status chip if you want one). Dropdown:Set("unknown")keeps the current selection instead of clearing it.- Input fires its callback on Enter even when the text did not change, and
RemoveTextAfterFocusLostfires once and keeps the submitted text asCurrentValue. - Controls in a row no longer pad each other to equal height;
_balance()and_relayout()are no-ops. - A slider suffix that is a word gets a space ("10 Bananas"); short suffixes stay attached ("50%", "3ms").
PruneRegistries()returns zero counts: there are no global registries left to prune.Argon._themeBindingsandArgon._connectionsstay as empty tables.- The library settings rows are views over the window preferences instead of flags. The 1.x
ArgonUI*flag names still work throughGetFlagValueandSetFlagValue. - Window size: the default is
max(min(gui - 160, 1080), min(720, gui - 16))wide bymax(min(gui - 110, 700), min(440, gui - 16))tall; the sidebar is 232 px and collapses to a 64 px icon rail. LoadConfiguration()returnstrue(with an info string) when there is no saved file yet, and when every value is parked for controls that do not exist yet.SetTheme(table)is remembered as a custom theme across sessions. Theme changes made from the picker are saved.OnFlagChangedunbinds by identity, so unbinding one listener never removes another.Element:Destroy()releases the flag (its last value is kept for the next save) and any keybinds the control owned.SetVisible(false)survives tab switches and search.- Lock and
SetDisabledblock every user path, including a control's keybind; a script'sSetstill works. - Closing a confirmation with x, the close key or an outside click calls
OnCancel. - Colour values read from configuration files without the
__argonColormarker are treated as 0-255 (Rayfield's format). - 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:GetColororArgon:GetPalette()instead of instance colours. - Overlay transparency (0.12-0.8 in 1.x) now maps onto the frosted HUD range, so overlays stay readable.
SetSmoothTransitions(false)turns on reduced motion.- Technical keeps Sarpanch; the default typeface is still Roboto with Montserrat headings (BuilderSans is one of the six sets).
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.
| Option | Type | Default | Description |
|---|---|---|---|
Name | string | "Argon UI" | Window name, shown in the sidebar, on the loader card and in messages such as "My Hub is hidden". |
Subtitle | string | "Control Center" | Line under the name, shown as given (no upper-casing). |
Logo | string or number | first letter of the name | rbxassetid://… or an asset id. Rayfield's Icon also works. |
Theme | string or table | "Argon" | A built-in theme, a Rayfield theme name, or a palette table. See Themes. |
Font | string | "Roboto" | Typeface set. See Appearance. |
ConfigurationSaving | table | disabled | { Enabled, FolderName, FileName, FileExtension }. |
GuiName | string | "ArgonUI" | ScreenGui name. Only a previous Argon window with this name is replaced. ScreenGuiName also works. |
DisplayOrder | number | 100 | The ScreenGui's DisplayOrder. |
ToggleUIKeybind | KeyCode or string | Insert | Hides 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. |
MouseUnlock | boolean | true | Frees the cursor in first-person and shift-lock games while the window is open. |
Cursor | boolean or "auto" | "auto" | Draws a pointer above the interface. "auto" only does so when the game has hidden the engine cursor. |
Scale | number 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. |
Density | string | "Auto" | "Compact" or "Touch". Auto follows the input device. |
ReducedMotion | boolean | false | Springs become instant, fades are capped at 100 ms and loops stop. |
ThemePicker | boolean | true | The Appearance button in the topbar. |
Palette | boolean | true | false turns the command palette off. |
PlayerList, KeybindList, Watermark | boolean | false | Overlays. All three are off unless you turn them on. See Overlays. |
OverlaysAlwaysVisible | boolean | false | Keeps the overlays on screen while the window is hidden. |
Loading | "manual" | automatic | Hold the window until you call FinishLoading. ManualLoading = true also works. See Loading. |
LoadingScreen | string or table | "auto" | "auto", "always", "never" or { Title, Subtitle, Tips }. Rayfield's LoadingTitle and LoadingSubtitle map onto it. |
LoadingMinimum | number | 0.25 | Seconds the loader card stays once it has appeared, so it never flickers. |
KeySystem, KeySettings | boolean, table | off | Rayfield's key gate. See Key system. |
Prewarm | boolean | true | Builds 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.
| Layout | When | Shape |
|---|---|---|
| Wide | window at least 1,080 px wide | 232 px sidebar, page column, and an "On this page" list on the right |
| Standard | 760 to 1,080 px | 232 px sidebar, page column, and a row of section chips under the page title |
| Icon rail | under 760 px, or the user collapsed the sidebar | a 64 px rail of icons; tooltips name each tab |
| Phone landscape | touch, short side under 500 px | the window fills the screen, with the rail and touch-sized rows |
| Phone portrait | touch, screen under 600 px wide | a 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 script | What happens |
|---|---|
| Builds everything in one go | The window opens in about 0.1 to 0.2 s with no card. |
Builds, then calls LoadConfiguration | The 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 tab | The card shows "Waiting for script"; after 8 s without progress it offers Open now and Copy log. |
| Waits in the middle of the build | After 250 ms of quiet the window opens with the tabs built so far; later tabs slide into the sidebar. |
Uses Loading = "manual" and never finishes | The 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)
| Member | Description |
|---|---|
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" }).
| Member | Description |
|---|---|
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.
Toggle
A switch with a check glyph, so state never depends on colour alone.
new · valueCheckbox
A toggle drawn as a checkbox.
valueSlider
A ranged number you drag, step with the keys, or type.
valueStepper
An exact number with minus and plus buttons.
valueInput
Single-line text with validation, numbers and limits.
valueTextArea
Multi-line text that can grow with its content.
valueDropdown
Single or multi select, searchable, any number of options.
valueSegmented
Two to four choices side by side.
valueKeybind
Key capture with modes, mouse and pad buttons, and conflicts.
valueColorPicker
HSV square, hue strip, optional alpha, presets and recents.
actionButton
An action row, or a Primary, Ghost, Danger or Danger soft pill.
new · actionButtonGroup
Two or three buttons side by side in one row.
displayProgressBar
Determinate, stepped or indeterminate progress.
displayList
Rows with inline actions, a filter, rename and an empty state.
displayTelemetryCard
A live status line with a coloured status pill.
new · displayStat
A big number with a unit, a delta pill and a sparkline.
new · displayCallout
A tinted info, success, warning or danger box with an action.
new · displayImage
An image with rounded corners and a caption.
displayLabel
A short line of text with an optional icon.
displayParagraph
A title above wrapped body text.
layoutDivider
A rule with an optional caption.
layoutSpace
An explicit vertical gap.
layoutSection
A header and one platter, with its own builders.
layoutRow
Up to four columns inside a section.
Every control config also accepts these keys:
| Key | What it does |
|---|---|
Flag | Saves the value under this name and makes it reachable through GetFlagValue and SetFlagValue. |
Description | A line under the title. Wraps to at most two lines on desktop. |
Tooltip | The same kind of text without using row height; an info mark after the title shows it on hover, focus or tap. |
Keywords | Extra words the command palette matches, e.g. { "esp", "wallhack" }. |
Pinned | On toggles and buttons: pins the control to the sidebar and the quick dock. |
Keybind | On toggles and buttons: a key that flips or presses it, e.g. Keybind = "F". |
ContextMenu | false turns the row menu off for this control. |
VisibleWhen / EnabledWhen | Shows or enables the control only while other flags match, e.g. { Flag = "Mode", Equals = "Advanced" }. |
Default | The 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 })
| Option | Description |
|---|---|
PlaceholderText / Placeholder | Shown while empty. |
Numeric, Min, Max | Numbers only; out-of-range values clamp on commit and the row says the allowed range. |
MaxLength | Limits 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. |
Live | Sends 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, ReadOnly | A clear button, a leading icon, hidden characters, and a read-only field with a copy button. |
RemoveTextAfterFocusLost | Clears 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 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)
| Variant | Look |
|---|---|
"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.
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
| Member | Description |
|---|---|
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, ShowValue | Segments 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 field | Type | Description |
|---|---|---|
Title | string | Primary row text. A plain string item becomes the title. |
Description | string | A second line; the row grows to fit. |
Actions | table | { Label, Callback, Style = "danger" | "warning", Color } per action. The first two show inline (InlineActions changes that); the rest sit behind the ellipsis menu. |
Action, Callback | string, function | The 1.x single trailing button and the row's own handler (falls back to the list Callback). |
Color | Color3 | Overrides 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.
| Key | What it does |
|---|---|
| ↑ ↓ | Move through the results. |
| Enter | Go to the control: switch tab (building it if needed), scroll it into view and highlight the row. Commands run. |
| Ctrl Enter | Run 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 B | Close 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.
| Keyboard | Gamepad | What it does |
|---|---|---|
Insert (or your ToggleUIKeybind) | Hide or show the window | |
| Ctrl K, / | Y | Command palette |
| Ctrl 1 to 9, Ctrl [ ] | LB / RB | Switch tabs |
| ↑ ↓, Tab | D-pad ↑ ↓ | Move the focus ring |
| ← → (Shift for ×10), Home End | D-pad ← → | Adjust the focused control |
| Space, Enter | A | Activate: flip, press, open the picker, start key capture |
| Shift F10 | X | Row menu |
| Ctrl Z | Undo the last reset or paste while its Undo pill is showing | |
| Backspace | B | Close the top popup, menu, sheet or dialog, then leave focus mode |
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.
| Member | Description |
|---|---|
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() · CurrentValue | The 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 · Flag | The 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
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.
| Theme | Kind | Frame | Surface | Text | Accent | AccentHi |
|---|---|---|---|---|---|---|
Argon | dark | #0C0E16 | #1F212A | #F1F3F9 | #9970FF | #C3AAFF |
Amethyst | dark | #100B1A | #241E2F | #F5F0FF | #A76DFF | #D2B2FF |
Midnight | dark | #080C15 | #1B202B | #EEF4FF | #58A6FF | #9BCDFF |
Emerald | dark | #080F0E | #1B2322 | #EEF9F6 | #34D399 | #97F6D3 |
Rose | dark | #130A10 | #281D24 | #FFEFF8 | #F472B6 | #FBB6DC |
Slate | dark | #0D1014 | #1F2329 | #EEF3F9 | #7DC4FF | #BADEFF |
Ember | dark | #120B08 | #261E1B | #FFF1E9 | #FF8A4C | #FFBE96 |
Nord | dark | #1E222B | #2F343F | #ECEFF4 | #88C0D0 | #BEE0E8 |
Mono | dark | #0C0C0E | #202022 | #F4F4F6 | #E2E2E8 | #FFFFFF |
Ocean | dark | #071018 | #1A242C | #E9F6FC | #38BDD0 | #96E7F0 |
Daylight | light | #E5E7EF | #FFFFFF | #12141C | #6B4AE6 | #7C5DED |
HighContrast | high 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.
| Token | 1.x name | Used for |
|---|---|---|
Frame | Background, Sidebar | The glass window frame, the sidebar and the icon rail |
Sheet | Window | The opaque content sheet: topbar, page and "On this page" |
Surface | Surface | Section platters, stat tiles, list bodies |
Hover | Hover | Row hover and menu rows |
Active | Active | Pressed and selected fills, Secondary buttons |
Raised | (new) | Floating layers: popovers, menus, toasts, dialogs, the palette, the loader card |
Well | Input | Recessed fields: inputs, dropdown triggers, value pills, segmented and stepper tracks |
Track | (new) | Switch-off track, slider groove, progress track |
Border | Border | Control edges |
BorderSoft | BorderSoft | Dividers |
Text | Text | Primary text |
Text2 | (new) | Secondary text; the only text colour besides Text used on the glass frame |
Muted | Muted | Descriptions and meta text |
Dim | Dim | Placeholders and hints |
Disabled | (new) | Disabled text and glyphs |
Accent | Accent | Brand and state colour |
AccentHi | AccentLight | Light end of the active gradient; the focus ring in dark themes |
AccentDeep | AccentDark | Pressed 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, Info | Success, Warning, Danger | Status 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 |
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
| Material | Frame | Sheet | Dock and orb | Overlays |
|---|---|---|---|---|
Solid | opaque | opaque | opaque | opaque |
Tinted (default) | 14 % see-through | opaque | 6 % | 10 % |
Clear | 25 % see-through | 6 % | 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.
| Density | Row height | Control height | Switch |
|---|---|---|---|
compact | 36 px (48 with a description) | 28 px | 40 × 22 |
| default (mouse) | 44 px (56) | 32 px | 44 × 24 |
touch | 52 px (64) | 40 px | 52 × 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.
| Set | Body | Titles |
|---|---|---|
Roboto (default) | Roboto | Montserrat |
BuilderSans | BuilderSans | BuilderSans |
Ubuntu | Ubuntu | Ubuntu |
Condensed | RobotoCondensed | Oswald |
Technical | Sarpanch | Sarpanch |
Gotham | GothamSSm | GothamSSm |
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)
| Section | Rows |
|---|---|
| Appearance | Theme, material, interface scale, density, state labels, reduce motion, typeface |
| Window | Menu name and subtitle, custom logo |
| Controls | Menu key, reopen button, mouse unlock |
| Notifications | Show callback errors, clear inbox |
| Overlays | Keybind list, player list, watermark, keep overlays on screen, opacity, scale, overlay hotkeys, reset positions |
| Profiles | The profile manager |
| About | Copy 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 })
| Option | Description |
|---|---|
Title, Content | The 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 / Image | Replaces the type's icon. |
Duration | Seconds. 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. |
Actions | Up to two buttons: { Text, Callback }. A button dismisses the card unless it has Keep = true. |
Id | Notifying 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.
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
Creates a window and returns at once. See Windows for the options. Creating a second window with the same GuiName replaces the first.
Shows or hides the last window created.
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.
A short notice. action is one { Text, Callback } button.
Configuration and profiles
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.
Writes and verifies now. With a name, saves the current values as that profile.
Deletes a profile or the default file; clears stored values and deletes the file while cancelling a queued autosave.
Profile management. Each list entry has Name, Path, Autoload, Count, SavedAt and Active.
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.
Reads a live value (or a stored one whose control is not built yet), applies a value through its control, and snapshots every flag.
Calls fn(flag, value, element) on user edits and restores. The returned function removes only this listener.
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
Applies a built-in theme, a Rayfield theme name or a palette table to every live control.
Registers a palette (missing tokens are derived), or generates a complete one from { Name, Accent, Mode, Neutral, Contrast }.
Uses your accent on any theme and re-derives the related tokens. nil restores the theme's own accent.
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.
Switches the typeface set live. See Typefaces.
Instant springs, fades capped at 100 ms, no loops.
Icons, diagnostics and teardown
Resolves a Lucide name for your own frames. See Icons.
What the library is doing right now. See Performance.
Kept for 1.x scripts. There are no global registries left, so it returns zero counts.
Saves, disconnects everything and removes every window.
Flags maps flag names to control objects, which keeps profile helpers simple. Version is "2.0.0" for this release.
Window API
| Method | Description |
|---|---|
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 · ActiveTab | Useful 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:
- Nothing slow happens before the window can draw. No icon download or parse, no avatar lookup, no base64 decode, no file write at load.
- Controls are state first. A
Create*call records the control in microseconds. Instances are built by a scheduler that spends a few milliseconds per frame: the visible tab first, the others in the background after the window opens. - No slowdown as the hub grows. 1.x scanned every colour against the palette and kept an ever-growing global list, so tab 30 built 7 to 11 times slower than tab 1. 2.0 keeps themed properties per component.
- Nothing runs at idle. One input handler per library, no per-frame work, no endless tweens and no periodic cleanup.
local stats = Argon:GetPerfStats()
print(stats.mountedElements, stats.pendingElements, stats.instancesCreated)
| Field | Meaning |
|---|---|
elements · mountedElements · pendingElements | Controls created, built, and still waiting to be built. |
instancesCreated | Instances the library has created so far. |
liveScopes | Live ownership scopes (each control, tab and layer owns one). |
globalHandlers | { InputBegan, InputChanged, InputEnded } handler counts. |
springsAndLoops · dragSessions · popoversOpen | Animations running, drags in progress, open popovers. All zero at idle. |
scheduler | The build scheduler's queue and timing. |
iconsDecoded · bundleVersion | Icons 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"
))()
| Rayfield | In Argon UI |
|---|---|
LoadingTitle, LoadingSubtitle | Shown on the loader card when one appears (LoadingScreen controls it). |
Icon on the window | Becomes the Logo; a number is treated as an asset id. |
KeySystem, KeySettings | Supported, plus Validate and GetKeyLink. |
| Theme names | Default, AmberGlow, Bloom, DarkBlue, Green, Serenity and Light map to generated or built-in themes. |
Keybind Callback | Fires on press, as in Rayfield; HoldToInteract passes true and false. |
CreateLabel(text, icon, color) | The same signature. |
| Saved colours | Files without the __argonColor marker are read as 0-255, Rayfield's format. |
| ScreenGui name | ArgonUI by default; override with GuiName. |
| Config file extension | FileName without an extension gets .rfld; an extension you give is kept. |
| Restored callbacks | Always fire once on load, even when the value matches the default. |
| Save results | SaveConfiguration returns ok, path or false, reason instead of only a boolean. |
| Sections | Optional; 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
tests/run.luauis the 1.x contract suite. Where 2.0 deliberately changes a pinned pixel value, colour or wording, the assertion was updated and commented with the reason;tests/README.mdlists every one.tests/v2_core.luaucovers lazy building, the scheduler, idle cost, the loader, input, persistence and profiles, change tracking, search, notifications and dialogs.tests/controls_a.luau,tests/controls_b.luauandtests/display.luaucover control behaviour and instance budgets.tests/sandbox.luausteps frames, computesAbsoluteSizeandAbsolutePosition, simulates input and models executor profiles. It models the API rather than rendering, so layout and feel still need a real client.
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
- Nothing slow happens before the window can paint. 1.x fetched and parsed the whole Lucide icon index with
HttpGet+loadstringon the first icon, calledGetUserThumbnailAsyncsynchronously insideCreateWindow, and decoded about 48 KB of base64 at load. 2.0 does none of that: all 1,573 Lucide icons are embedded as an 18 KB table, the avatar is anrbxthumb://URL, and there is no base64 or file write at load. - Controls are state first.
Create*builds a plain table in microseconds. Instances are created later by a frame-budgeted scheduler: the visible tab first, the others in the background after the window opens. Reading an instance field such aselement.Rootstill works; it builds that control on the spot. - No quadratic slowdown. 1.x scanned every colour against the palette and kept an ever-growing global binding list, so tab 30 built 7-11 times slower than tab 1. 2.0 records themed properties per component, with no global registry.
- Nothing runs at idle. One
InputBeganhandler per library (1.x had about 40, plus 43InputChangedand 43InputEnded), noRenderStepped/Heartbeatwork, no endless tweens and no 30-second janitor. - Measured numbers are in
bench/RESULTS.md.
An honest loader
- No fixed timers. Nothing is shown for the first 150 ms; a card appears only while real work is still pending, says what it is doing ("Building Combat · 155 of 212", "Restoring settings · 72 of 96"), and hands over to the already-built window with a short morph. A fast build opens with no card at all.
- The window opens when the host is done: its script finished, it called
FinishLoading, or it built a tab and went quiet for 250 ms. Manual mode (Loading = "manual", or anySetLoadingProgresscall) still waits forFinishLoading; a stall footer with Open now and Copy log appears after 8 s without progress, and the watchdog opens the window 30 s after the last progress. - New:
LoadingScreen = "auto" | "always" | "never" | {Title, Subtitle, Tips},LoadingMinimum,Window:SetLoadingStatus(text),Window:OnLoaded(fn), andWindow:Build(fn), which shows a build error on the loader with Open anyway and Copy error.
A new look
- A tinted glass frame with an opaque sheet and grouped section platters, a lit edge, light only on active state (switch on, selected tab, primary button, focus), and Montserrat headings over Roboto rows. Radii are concentric (20/14/10).
- 12 themes: the ten 1.x themes with their accents kept, plus Daylight and HighContrast. Every theme meets the contrast targets in
design/v2/DESIGN_V2.md(text 10:1 on platters, secondary text 9:1). - Material choice (
Solid,Tinted,Clear), an accent override (Argon:SetAccent), interface scale (Window:SetScale), density (Window:SetDensity: compact, touch) and reduced motion. - Responsive layouts: wide (with an "On this page" table of contents), standard, a 64 px icon rail, phone landscape, and phone portrait with a bottom tab bar.
New features
- Command palette (Ctrl+K,
/over the window, the sidebar search, gamepad Y). It finds controls in tabs that were never opened; Enter goes to a control, Ctrl+Enter runs it in place, Left/Right adjust it in place.Window:RegisterCommand{...},Window:OpenPalette(query), andKeywords = {...}on any control. - Row context menu (right-click, long-press, Shift+F10, gamepad X): reset to default, copy and paste value, copy flag name, pin, show in palette, plus
Element:AddMenuItem{...}. - Change tracking with undo: a dot on changed rows, "N changed · Reset tab" in the page header, Reset section, and an Undo pill (Ctrl+Z).
Element.Default,Element:IsChanged(),Element:Reset(),Section:Reset(),Tab:Reset(),Tab:GetChangedCount(),Window:ResetAll(). - Pins: pin switches and buttons to a Pinned sidebar group and a quick dock that stays on screen while the window is hidden.
Pinned = truein a control config. - Profiles:
SaveConfiguration(name),LoadConfiguration(name),ListConfigurations,RenameConfiguration,DeleteConfiguration(name),SetAutoloadConfiguration,ExportConfiguration(anARGON2:code) andImportConfiguration;Window:CreateConfigManager(tab)adds a manager. - Notifications v2:
Type(info,success,warning,error,loading),Actions(up to two buttons),Id(update in place),Image/Icon, automatic duration, a timer that pauses on hover, identical toasts coalescing, at most four on screen, and an inbox behind the topbar bell.Notifyreturns a handle withUpdate,DismissandSetProgress, and it now works beforeCreateWindow.Argon:Toast(text, type, action)for small notices. - Callback errors become an error notification with Copy error and Retry instead of a silent warning.
- Keyboard and gamepad: focus mode with a focus ring, arrow-key navigation and adjustment, Ctrl+1-9 and Ctrl+[ ] to switch tabs, gamepad hint bar. Backspace and gamepad B close the top layer.
- Dialogs:
Window:Prompt{Title, Content, Placeholder, Default, Validate, OnSubmit, OnCancel}; Enter activates the focused button; dialogs close on an outside click. - New controls and blocks:
CreateCallout,CreateStat(with sparkline),CreateImage,CreateButtonGroup,CreateCheckbox(a toggle withStyle = "Checkbox"), button variants (Primary,Ghost,Danger,DangerSoft),Keybind = "F"on toggles and buttons. - Status and badges:
Window:SetStatus(text, tone),Tab:SetBadge(n | "New" | true), and tab groups withCreateTab(name, icon, { Group = "...", Description = "..." }). - Key gate: Rayfield's
KeySystem/KeySettings, plusValidateandGetKeyLink. - Settings tab:
Window:CreateLibrarySettings(tab)now covers appearance, window name and logo, the menu key, the reopen button, mouse unlock, notifications, overlays, profiles and diagnostics. The account menu and the palette open it even when the host never created one. - Overlays: a watermark (fps, ping, time) next to the player list and the keybind list.
Window:SelectTab,GetActiveTab,IsVisible,SetMaterial,SetMouseUnlock,SetOpenButton,Destroy;Argon:SetVisibility,IsVisible,CreateTheme,GetPerfStats;Section:Set,SetCollapsed;Divider:Set;Button:Set;ColorPicker.Color;Paragraph:Set({Title, Content});CreateLabel(text, icon, color).
Changes you might notice when upgrading from 1.x
The full list of 25 behaviour changes is under Upgrading from 1.x.
Tests
tests/run.luauis still the 1.x contract suite. Where 2.0 deliberately changes a pinned pixel value, colour or wording, the assertion was updated and commented with the reason;tests/README.mdlists every one.- New suites:
tests/v2_core.luau(lazy mounting, scheduler, idle cost, loader, input, persistence and profiles, change tracking, search, notifications, dialogs),tests/controls_a.luau,tests/controls_b.luau,tests/display.luau(control behaviour and instance budgets),tests/readme.luau(every README example runs), andtests/run_all.luauto run them all. tests/sandbox.luaunow steps frames, computesAbsoluteSizeandAbsolutePosition, has a class hierarchy, a recursiveDestroy, the full enums the library reads, input simulation, executor profiles and metrics.
1.18.3
The last 1.x release. Earlier changes are in the repository's git history and in CHANGELOG.md.