No dependencies and no host script. One .luau file: load it, create a window, add controls. The API is shaped like Rayfield's, so existing menus usually only need their loader swapped.
Documentation · Try it in your browser · Controls · Command palette · Themes · Upgrading from 1.x · FAQ
Every control in the docs is shown as a mockup next to the call that produces it.
local Argon = loadstring(game:HttpGet(
"https://raw.githubusercontent.com/JirkaachS/argon-ui/main/argon-ui.luau"
))()
local Window = Argon:CreateWindow({
Name = "My Hub",
ConfigurationSaving = { Enabled = true, FolderName = "MyHub", FileName = "config" },
})
local Tab = Window:CreateTab("Main", "zap")
Tab:CreateSection("Farming")
Tab:CreateToggle({
Flag = "AutoFarm",
Name = "Auto farm",
Callback = function(enabled) print("auto farm:", enabled) end,
})
Argon:LoadConfiguration()That is the whole setup. The window opens by itself as soon as your script is done building. Keep a copy of argon-ui.luau beside your script if you want an offline fallback for the hosted URL.
Most Roblox UI libraries give you a window and a list of toggles and stop there. Argon UI is built for hubs that outgrew that: hundreds of controls, long tabs, sessions that need to survive a rejoin, and games that fight your menu.
- It opens fast, however big the hub is.
Create*calls only record what you asked for. The visible tab is built first, in small slices that never hold up a frame, and the other tabs are built quietly after the window is open. Nothing is downloaded, decoded or written to disk before the window can draw. - A loader that tells the truth. No fixed timers. A quick script opens with no loader at all; a big one shows what is actually happening ("Building Combat · 155 of 212", "Restoring settings · 72 of 96") and opens the moment the work is done.
- Find anything. The command palette (Ctrl+K) finds any control in any tab, even tabs you never opened, and can change it in place.
- Undo, reset, profiles. Changed rows are marked, every tab can be reset, Ctrl+Z undoes, and settings can be saved as named profiles and shared as a code.
- Persistence that round-trips. Writes are read back to confirm they landed, values for controls that do not exist yet are kept, and restored values fire their callback once.
- Notifications outside the window, with types, buttons, progress and an inbox, so a background feature can report while the menu is hidden.
- Its own cursor, for games that hide the engine pointer behind their own.
- Keyboard, gamepad and touch work everywhere: focus ring, arrow keys, shoulder buttons, long-press menus, phone layouts.
- Rayfield-compatible API, 12 live-switchable themes, 1,573 Lucide icons built in, and a browser preview that is a working reimplementation rather than a screenshot.
- Tabbed window with sections, rows of up to four columns, tab groups, badges, and a jump rail and "On this page" list for long pages
- 22 control builders: switches, checkboxes, sliders, steppers, segmented pickers, dropdowns, colour pickers, keybinds, inputs, text areas, buttons, button groups, lists, progress bars, telemetry and stat cards, callouts, images, labels, paragraphs, dividers and spacers
- Command palette, row context menus, change tracking with undo, pins and a quick dock
- Named configuration profiles with autoload, export and import
- Typed notifications with actions, progress, update in place and an inbox
- Dialogs, confirmations and prompts; tooltips, descriptions, locks and conditional visibility
- 12 themes plus your own, accent override, three materials, interface scale and density
- Player list, keybind list and watermark overlays that can stay on screen while the window is hidden
- Key system (Rayfield's
KeySystemsettings) - Responsive: wide, standard, icon rail and phone layouts, with a
560 x 400minimum window
| Group | Builders |
|---|---|
| Input | Toggle, Checkbox, Input, TextArea, Keybind |
| Choice | Dropdown, Segmented, ColorPicker |
| Numeric | Slider, Stepper |
| Action | Button, ButtonGroup |
| Display | Label, Paragraph, Callout, Image, ProgressBar, List, TelemetryCard, Stat |
| Layout | Section, Row, Divider, Space |
Every Create* method has a prefix-free alias (Tab:Toggle, Section:Dropdown, and so on), and every control config also accepts:
| Key | What it does |
|---|---|
Description |
A line of text under the title. |
Tooltip |
The same text without spending row height; an info mark after the title shows it on hover or focus. |
Keywords |
Extra words the command palette matches. |
Pinned |
On toggles and buttons: pins it to the sidebar and the quick dock. |
VisibleWhen / EnabledWhen |
Shows or enables the control only when other flags match, e.g. { Flag = "Mode", Equals = "Advanced" }. |
Keybind |
On toggles and buttons: a key that flips or presses it, e.g. Keybind = "F". |
Buttons take Variant = "Primary" | "Ghost" | "Danger" | "DangerSoft", HoldToConfirm = true (hold for 0.8 s, for destructive actions) and Async = true (a spinner while the callback runs, a check when it returns true). Sliders take Ticks and a Format function for the value.
A tab with ninety controls is a single column you scroll blind, and the related ones end up screens apart. CreateRow splits the section body into columns and hands controls out round-robin, so the pair you build together sits side by side.
local row = Tab:CreateRow({ Columns = 2 })
row:CreateToggle({ Flag = "Chams", Name = "Chams", CurrentValue = false })
row:CreateSlider({ Flag = "ChamsOpacity", Name = "Opacity %", Range = { 5, 100 }, Increment = 1, CurrentValue = 60 })
-- or, 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 columns wrap back into one stack, so a resized window and a phone get a readable layout from the same calls. Rows carry the same shorthands as sections and join the owning section for search.
Best suited to compact controls: toggles, sliders, steppers, keybinds, colour pickers, buttons, inputs, dropdowns and short segmented pickers. Lists and text areas want the full width.
Press Ctrl+K, press / with the pointer over the window, click the search field 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.
- Enter goes to the control and highlights it. Ctrl+Enter runs it in place (flips a switch, presses a button). Left and Right adjust sliders, steppers and pickers without leaving the palette.
- Recent picks come first.
Keywords = { ... }on a control adds words it should match. - Add your own commands:
local command = Window:RegisterCommand({
Name = "Rejoin server",
Icon = "rotate-ccw",
Keywords = { "reconnect", "server hop" },
Callback = function() rejoin() end,
})
command.Remove() -- take it out again
Window:OpenPalette("speed") -- open it from code, pre-filledEvery control knows its default. A changed row gets a dot, the page header says "3 changed · Reset tab", and a reset or a profile load can be undone with the Undo pill or Ctrl+Z.
toggle:IsChanged() -- true when the value differs from the default
toggle:Reset() -- back to the default
Tab:GetChangedCount()
Tab:Reset()
Window:ResetAll()Right-click a row (long-press on touch, Shift+F10 or gamepad X) for its menu: reset to default, copy and paste the value, copy the flag name, pin, and show in the palette. Add your own items with element:AddMenuItem({ Text = "Explain", Callback = function() ... end }).
Pinned switches and buttons get a Pinned group at the top of the sidebar and a quick dock that stays on screen while the window is hidden, so the three toggles you use mid-fight are one tap away.
Tooltip carries the same explanatory text as Description without spending row height on it, which matters once controls share a row. A control with a tooltip is marked with a small info glyph after its title. The panel wraps, sizes to its content, and opens beside the row it explains, flipping when there is no room. It opens after a short delay so sweeping down a column does not strobe, and immediately when you move from one tooltip to the next. Keep Description for anything a user must read before acting.
Tab:CreateToggle({ Name = "Kill Aura", Tooltip = "Hits every nearby enemy. Cannot run with Aimbot.", CurrentValue = false })Lock states a precondition on the control itself instead of failing after the click. The row is dimmed and shows a padlock chip with the reason; a click on the row shows the reason instead of changing the value, and the control's keybind is blocked too. A script's own Set still works.
element:Lock("enter a container ID") -- greyed, with the reason on the row
element:SetLocked(conquestIsOn, "Conquest Auto-Hop is on")
element:Unlock()Window:Confirm is a one-shot gate for destructive actions. Cancel has the focus, the confirm button is tinted as a danger, and the dialog destroys itself once a choice is made. Closing it any other way (the x, Backspace, gamepad B, a click outside) calls OnCancel.
Window:Confirm({
Title = "Empty container?",
Content = "Every item type is pulled out at full quantity. There is no undo.",
ConfirmText = "Empty it",
OnConfirm = function() emptyContainer() end,
})
Window:Prompt({
Title = "Rename profile",
Placeholder = "Profile name",
Validate = function(text) return #text >= 2, "Use at least 2 letters" end,
OnSubmit = function(text) print("new name:", text) end,
})A list row can carry several buttons instead of one, which beats a selectable list plus a stack of buttons that act on "whatever is selected".
Tab:CreateList({ Name = "Profiles", Items = {
{ Title = "Mining", Description = "saved snapshot", Actions = {
{ Label = "Load", Callback = function(row) load(row.Title) end },
{ Label = "Rename", Style = "warning", Callback = function(row) rename(row.Title) end },
{ Label = "Delete", Style = "danger", Callback = function(row) remove(row.Title) end },
} },
} })Style accepts danger and warning, or pass an explicit Color. A single Action = "Load" with a row-level Callback still works. Long lists only build the rows that are on screen.
You do not have to do anything. While your script builds, the window stays hidden; it opens when your script finishes, or when it has built a tab and gone quiet for a quarter of a second. Nothing is shown for the first 150 ms, so a quick script opens with no loader card at all. A big one gets a small card that says what is being built and restored.
To drive the loader yourself, report progress and finish:
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()Progress never goes backwards. Window:SetLoadingStatus(text) changes only the text, Window:OnLoaded(fn) runs once the window is open, and Window:Build(fn) runs your builder and shows any error on the loader with Open anyway and Copy error buttons. If a manual build stops reporting for 8 seconds, the card offers Open now; 30 seconds after the last progress the window opens by itself.
LoadingScreen = "never" skips the card entirely, "always" shows it even for a quick build, and { Title = ..., Subtitle = ..., Tips = { ... } } brands it. LoadingTitle and LoadingSubtitle (Rayfield's names) still work.
Argon:Notify shows a card in the top-right corner. It is deliberately not tied to window visibility, and it works before CreateWindow.
Argon:Notify({ Title = "Saved", Content = "Configuration written.", Type = "success" })
local job = Argon:Notify({
Id = "download",
Title = "Downloading map",
Type = "loading",
Actions = { { Text = "Cancel", Callback = function() cancelDownload() end } },
})
job:SetProgress(0.4)
job:Update({ Title = "Map ready", Type = "success" })Type is info, success, warning, error or loading; without it the type is guessed from whole words in the title ("Save failed" reads as an error, "No errors found" does not). The duration follows the length of the text unless you pass Duration, and the timer pauses while the pointer is over the card. Identical cards within two seconds merge into one with a count, at most four are on screen, and everything also lands in the inbox behind the bell in the topbar. Notifying again with the same Id updates the card in place.
An error inside one of your callbacks becomes an error card with Copy error and Retry instead of a silent warning in the console. Turn that off in the Settings tab.
Label, Paragraph, Callout and TelemetryCard size themselves to their content. A long body wraps and the row grows; a short one does not leave an empty slot behind.
Input shows an elided value while it is not focused, dropping the scheme and keeping the informative head and tail, and restores the real text for editing on focus. CurrentValue is always the full string, so a saved webhook URL round-trips exactly even though the field never displays it in full. The callback fires when the text changed or when Enter is pressed.
local field = Tab:CreateInput({ Flag = "Webhook", Name = "Webhook URL", PlaceholderText = "https://…" })
field:Set("https://discord.com/api/webhooks/1523331338155982949/RvztQ8kLmNoPqRs")
print(field.CurrentValue) -- the whole URL, not the elided displayAny control given a Flag is saved to FolderName/FileName shortly after it changes, and each write is read back to confirm it landed. 1.x configuration files load unchanged.
Argon:LoadConfiguration() -- restores values, runs callbacks once, enables autosave
Argon:GetFlagValue("AutoFarm")
Argon:SetFlagValue("FlySpeed", 220)
Argon:OnFlagChanged(function(flag, value) print(flag, value) end)
Argon:GetConfigurationStats()
Argon:ResetConfiguration()Restored values always run their callback once, even when the stored value equals the control default, and values whose control does not exist yet are kept rather than dropped.
Profiles are named configurations next to the main file:
Argon:SaveConfiguration("PvP") -- save the current values as a profile
Argon:LoadConfiguration("PvP")
Argon:ListConfigurations() -- { { Name = "Default", ... }, { Name = "PvP", ... } }
Argon:RenameConfiguration("PvP", "Arena")
Argon:SetAutoloadConfiguration("Arena") -- loaded by LoadConfiguration() from now on
local code = Argon:ExportConfiguration("Arena") -- an "ARGON2:..." string to share
Argon:ImportConfiguration(code)
Argon:DeleteConfiguration("Arena")
Window:CreateConfigManager(Tab) -- a ready-made manager with all of the aboveWindow preferences (theme, accent, scale, size and position, overlays, pins) are kept in a small .meta.json file beside the configuration, so they survive even without ConfigurationSaving.
A player list, a keybind list and a watermark (fps, ping, time). They are draggable and can stay on screen while the main window is hidden. All three are off unless you turn them on.
Argon:CreateWindow({ Name = "My Hub", PlayerList = true, KeybindList = true })
Window:SetWatermarkVisible(true)
Window:SetOverlaysAlwaysVisible(true)
Window:TogglePlayerList()
Window:ToggleKeybindList()The keybind list shows every keybind (active ones highlighted) and hides itself when there are none.
Window:CreateLibrarySettings(tab) adds a complete settings page in one call: theme, material, scale, density, reduced motion and typeface; the window name and logo; the menu key, the reopen button and mouse unlock; notifications; overlays; profiles; and a diagnostics copy. The account menu and the palette can open it even if you never create one.
Twelve built-in themes: Argon, Amethyst, Midnight, Emerald, Rose, Slate, Ember, Nord, Mono, Ocean, plus the light Daylight and HighContrast. Each one meets fixed contrast targets for text and secondary text. Rayfield theme names (Default, AmberGlow, Bloom, DarkBlue, Green, Serenity, Light) map to the closest theme.
Argon:SetTheme("Nord")
Argon:SetAccent(Color3.fromRGB(255, 120, 60)) -- your accent on any theme; nil to go back
Window:SetMaterial("Clear") -- "Solid", "Tinted" or "Clear"
Window:SetScale(1.1) -- 0.75 to 1.5
Window:SetDensity("Compact") -- "Compact", "Touch", or "Auto" to follow the device
Argon:CreateTheme({ Name = "Coral", Accent = Color3.fromRGB(255, 111, 97), Mode = "Dark" })
Argon:AddTheme("Mine", { Accent = Color3.fromRGB(0, 200, 160) }) -- missing keys are derivedEvery window gets a theme picker in the topbar. Hovering a theme previews it on the window, and the picker also sets the accent, material, scale and density. ThemePicker = false in CreateWindow hides the button; Window:ToggleThemePicker() opens it from code.
Every control the library builds re-themes itself. If your script builds its own frames outside the library, read the live palette instead of baking in colours:
local rowColour = Argon:GetColor("Surface")
local unbind = Argon:OnThemeChanged(function(palette, themeName)
myFrame.BackgroundColor3 = palette.Surface
end)Argon:GetColor(key, fallback) resolves a single key, Argon:GetPalette() returns a detached snapshot, and Argon.Palette is the live table. Main keys: Frame, Sheet, Surface, Hover, Well, Border, Text, Text2, Muted, Accent, AccentHi, AccentDeep, Success, Warning, Danger. The 1.x names (Background, Window, Sidebar, Input, AccentLight, AccentDark, Dim, and so on) still work.
All 1,573 Lucide icons are built in. Nothing is fetched, so icons draw on the first frame, offline too.
Window:CreateTab("Mining", "pickaxe")
local icon = Argon:GetIcon("shield-check") -- { Image, ImageRectOffset, ImageRectSize }
Argon:HasIcon("shield-check") -- trueA Roblox asset id ("rbxassetid://..." or a number) works anywhere an icon name does.
| Keys | What they do |
|---|---|
Insert (or your ToggleUIKeybind) |
Hide or show the window |
Ctrl+K, / |
Command palette |
| Ctrl+1 to 9, Ctrl+[ and ] | Switch tabs |
| Arrow keys, Tab | Move the focus ring; Left and Right adjust the focused control |
| Enter, Space | Activate the focused control |
| Shift+F10 | Row menu |
| Ctrl+Z | Undo the last reset or profile load |
| Backspace | Close the top popup, menu or dialog |
| Gamepad | L1/R1 tabs, Y palette, X row menu, B back |
Escape is left alone on purpose: in Roblox it opens the game menu.
Replace the file; your script and your users' saved configurations keep working. A few things look or behave differently on purpose, for example the loader has no fixed timers, the overlays are off by default, Escape no longer closes things, and a keybind's Callback fires on press as in Rayfield. CHANGELOG.md lists every one.
Usually by swapping the loader. The API shape follows Rayfield's, including CreateWindow, CreateTab, CreateSection, the Create* control methods with Flag, CurrentValue and Callback, Rayfield's theme names and its KeySystem settings. Anything Argon adds is optional.
An environment that provides loadstring and game:HttpGet (or the file pasted into a LocalScript in Studio). The window goes into gethui(), CoreGui or PlayerGui, whichever is available. Configuration saving needs file functions (writefile, readfile, isfile); without them the library runs fine and simply does not persist.
Yes. That is what 2.0 was rebuilt for. A window with 400 controls is created without building any of them up front, the visible tab is built first, and the library does no work at all while it sits idle. Measured numbers against 1.x are in bench/RESULTS.md.
Yes. Rows wrap to a single stack below MinColumnWidth, the sidebar becomes an icon rail on narrow windows, phones get their own landscape and portrait layouts, and every control has a touch size. Tooltips also open on a long-press, but put anything essential in Description.
The file you load is one file, so it drops into a single-file script and can be vendored beside it. The source is split into small modules in src/ and bundled into argon-ui.luau by build.luau.
Open the live preview. It is a working reimplementation rather than a screenshot: 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.
tests/ runs the real library outside Roblox against a Roblox API sandbox. Install Lune, then lune run tests/run_all.luau. See tests/README.md.
Issues and pull requests are welcome. Edit the modules in src/, then:
lune run build.luau # writes argon-ui.luau
lune run tests/run_all.luau # every suiteCommit the rebuilt argon-ui.luau together with your change. The design behind 2.0 is in design/v2/.
Also useful if you searched for: Roblox UI library, Luau GUI library, Rayfield alternative, Roblox script hub UI, Roblox menu library single file, Luau interface library with config saving, Roblox UI library with themes, Lucide icons for Roblox, responsive Roblox GUI, Roblox command palette, fast Roblox UI library.