Install

Lyra UI is one Luau file that returns the Lyra table. Load it in your executor:

Lua
local Lyra = loadstring(game:HttpGet("https://raw.githubusercontent.com/lyraEz/Library-UI/main/src/Lyra.lua"))()
In Roblox Studio, put src/Lyra.lua in a ModuleScript and require it from a LocalScript. Saving and the embedded icon sheet need executor functions (writefile, getcustomasset), so in Studio the window runs without saving and draws vector icons.

First window

Create a window, add a tab, fill it with elements. Every element returns an object you can keep to read or change it later.

local Window = Lyra:CreateWindow({
    Title = "My Hub",
    Subtitle = "by you  ·  v1.0",
    ChatCommand = "hub",
})

local Main = Window:Tab({Title = "Main", Icon = "star"})
Main:Section("Main", "Your features go here.")

local enabled = Main:Toggle({Title = "Enabled", Flag = "Enabled", Callback = function(on)
    print("Enabled:", on)
end})

Main:Button({Title = "Turn it off", Icon = "x", Callback = function()
    enabled:Set(false)
end})

Window:SettingsTab()

Callbacks run in their own thread, so they may yield. If one errors, the window keeps working and the error is shown in the status line and the console.

Flags & config

Give an element a Flag and its value is stored in Window.Flags, written to LyraUI/<Name>.json with writefile and restored the next time the script runs. When a saved value is restored, the element's callback runs once with it, so your feature turns back on by itself.

The same file keeps the interface settings: window size, interface size, theme, blur, reduced motion, the reopen options and the shortcut key.

Main:Slider({Title = "Power", Min = 0, Max = 100, Flag = "Power"})

print(Window:GetFlag("Power"))   -- read
Window:SetFlag("Power", 75)      -- moves the slider and runs its callback
Window:Save()                    -- write now (it also saves on its own, 0.4 s after a change)

-- Pick the file, or turn saving off:
Lyra:CreateWindow({Title = "My Hub", ConfigFile = "MyHub/settings"})
Lyra:CreateWindow({Title = "My Hub", ConfigFile = false})

Only booleans, numbers and strings are saved. A dropdown whose values are tables still works; it just starts from its default each run.

Window options

Lyra:CreateWindow(options) → Window · also Lyra.new(options)
OptionTypeDefaultWhat it does
Titlestring"Lyra UI"Header title. Also the default Name.
Subtitlestring"Lyra UI · v1.0.0"Small line under the title.
NamestringTitleIdentity of the window. Running a script again with the same name replaces the old window. Also names the config file.
Themestring | table"Lavender"A built-in theme name or a table of colours (see Themes). A theme the player picked in Settings wins.
SizeVector2 | UDim2 | {W, H}820 × 600Default size in unscaled pixels. Phones open full height at 560 wide.
Scalenumber0 (Auto)Interface size in percent (50–150). Auto is 100%, or 94% on phones.
KeybindKeyCode | stringRightControlKey that shows and hides the window.
ChatCommandstringnone"hub" makes /hub and /e hub reopen the window.
FloatingButtonbooleantrueDraggable button shown while the window is closed.
MenuReopenbooleantrueOpening and closing the Roblox menu 3 times within 5 s reopens a hidden window.
BlurbooleantrueBlurs the game behind the open window.
StartHiddenbooleanfalseStarts closed. Skipped when nothing could reopen the window.
NotificationsbooleantrueShows toasts.
ReducedMotionbooleanfalseTurns off window animations.
LowEffectsbooleanfalsePerformance mode: no blur, no glass, no animation.
ConfigFilestring | false"LyraUI/<Name>.json"Where flags and settings are saved. false turns saving off.
Logoasset id | stringnoneImage for the brand mark and the floating button.
LogoTextstringinitials of TitleLetters shown when there is no Logo.
NavCaptionstring"WORKSPACE"Caption above the tabs.
FooterstringTitle in capitalsText at the bottom of the rail.
Statusstring"Ready."First text of the status line.
CloseAction"Hide" | "Destroy""Hide""Destroy" makes the X ask, then close the window for good.
ParentInstancegethui() or PlayerGuiWhere the ScreenGui goes.
DisplayOrdernumber990ScreenGui display order.
OnVisibilityChangedfunction(visible)noneRuns when the window opens or closes.
OnDestroyfunction()noneRuns after Window:Destroy().

Window methods

MethodWhat it does
Tab(spec)Adds a tab. See Tabs. Alias: AddTab.
SettingsTab(spec)Adds the built-in Settings tab and returns it.
SelectTab(tab | title)Opens a tab.
Notify(spec)Shows a toast and returns a handle with Dismiss().
Status(text, kind)Sets the status line. Kinds: info, success, warning, error, loading.
Confirm(spec)Asks a question inside the window. Alias: Dialog.
Show() · Hide() · Toggle()Open or close with the flight animation. Hide also says how to reopen when there is no floating button.
SetVisible(bool)Opens or closes without the hint.
Minimize(bool) · Maximize(bool)Collapse to the header, or fill the screen.
SetTheme(name | table)Repaints every instance. GetTheme() returns the colours and the name.
SetTitle(title, subtitle)Changes the header text.
SetScale(percent)Interface size; 0 is Auto.
ResetSize()Back to the default size and interface size, centred.
GetFlag(name) · SetFlag(name, value)Read or change a flagged element. Window.Flags holds every value.
Save()Writes the config file now.
Command(name, fn)Chat command: /name args or /e name args calls fn(args).
Connect(signal, fn)Connects a signal and disconnects it when the window is destroyed.
Destroy()Removes the window, the blur, its chat commands and connections.

Tabs

Window:Tab({Title, Icon, OnSelect, Gap}) → Tab · Window:Tab(title, icon)

A tab is a scrolling page with a button on the side rail. The rail is 196 px with labels on wide windows, a 60 px icon rail below 760 px and a drawer behind a menu button below 520 px.

Tabs, groups, columns and collapsible sections are all containers: they take the same element methods. A tab also has Select() and SetVisible(bool), which takes it off the rail.

local Players = Window:Tab({Title = "Players", Icon = "users", OnSelect = function()
    refreshList()
end})

Settings tab

Window:SettingsTab({Title, Icon, Description, Themes}) → Tab

Adds interface size, window size reset, theme, notifications, start hidden, a live list of the ways to reopen the window, the floating button, the Roblox menu shortcut and the shortcut key, plus blur, reduced motion and performance mode under "Show advanced options". Add your own options to the returned tab. Themes = false hides the theme picker.

local Settings = Window:SettingsTab()
Settings:Section("Script")
Settings:Toggle({Title = "Auto execute", Flag = "AutoExecute"})

Notifications

Window:Notify({Title, Text, Kind, Icon, Image, Duration, Action, Featured, Dismissible, Force}) → handle
FieldWhat it does
TitleRequired. A string alone also works: Window:Notify("Saved").
TextSecond line; wraps.
Kind"info" (default), "success", "warning" or "error". Sets colour and glyph.
IconIcon name drawn in the badge instead of the kind's glyph.
ImagePicture in the badge.
DurationSeconds, default 4. 0 keeps it until tapped.
Action{Text = "Open", Callback = fn}: a button under the text.
FeaturedWider card, larger badge, stronger accent.
Dismissiblefalse: a tap on the card does not close it.
ForceShows even when the player turned notifications off.

Up to four toasts stack in the bottom-right corner; the oldest leaves when a fifth arrives. A hairline under each one shows the time left.

Confirm dialog

Window:Confirm({Title, Text, Confirm, Cancel, Seconds, Callback, OnCancel}) · Window:Confirm(title, text, callback)
Window:Confirm({
    Title = "Reset everything?",
    Text = "Every saved value goes back to its default.",
    Confirm = "Reset", Cancel = "Keep",
    Seconds = 10, -- cancels on its own, counting down on the button
    Callback = resetAll,
})

Reopen & chat

A closed window comes back through any of these, and Settings shows which of them work on the player's device:

  • Floating button: on by default, draggable.
  • Shortcut key: RightControl unless you set Keybind; players can change it in Settings.
  • Roblox menu: opening and closing it 3 times within 5 s. A toast explains it and offers to turn it off.
  • Chat: /hub or /e hub with ChatCommand = "hub". Not offered on accounts that cannot chat.

With the floating button off, the first close asks for confirmation and names the ways back. If none would work, the close button offers to turn the Roblox menu shortcut on first.

Window:Command("tp", function(args)
    -- "/tp spawn" or "/e tp spawn" runs this with args = "spawn"
end)

Elements

Call these on any container (tab, group, column, collapsible section). Each returns an object with Root, SetVisible(bool) and Destroy(); elements that hold a value add Set(value, silent) and Get(). silent = true skips the callback.

Every name also exists with an Add prefix (Tab:AddToggle) for people coming from other libraries.

Section, Paragraph, Label, Divider

Section(title, description) · Paragraph(text, {Muted, Size, Bold, RichText, Color}) · Label(text, color) · Divider()
Tab:Section("Farming", "Everything that runs on its own.")
Tab:Paragraph("Rich <b>text</b> works when you ask for it.", {RichText = true, Muted = true})
local count = Tab:Label("0 items")
count:Set("12 items", "Green") -- text and an optional theme colour name
Tab:Divider()

Button, ButtonRow, Action

Button({Title, Icon, Primary, Callback, Height, Enabled}) → {SetText, SetEnabled, SetPrimary, Fire}
local run = Tab:Button({Title = "Run", Icon = "arrow", Primary = true, Callback = run})
run:SetEnabled(false)

-- Equal-width buttons in one row; on narrow windows they drop their icons.
Tab:ButtonRow({
    {Title = "Undo", Icon = "undo", Callback = undo},
    {Title = "Customize", Icon = "pencil", Primary = true, Callback = customize},
    {Title = "Restore", Icon = "reset", Callback = restore},
})

-- A settings card with a button on the right.
Tab:Action({Title = "Window size", Description = "Back to the default.", Button = "Reset", Icon = "reset", Callback = reset})

Toggle

Toggle({Title, Description, Default, Flag, Callback(on)}) → {Set, Get, Toggle}
local fly = Tab:Toggle({Title = "Glide", Description = "Slower falls.", Default = false, Flag = "Glide",
    Callback = function(on) setGlide(on) end})
fly:Set(true)

Slider

Slider({Title, Description, Min, Max, Step, Default, Suffix, Format, Flag, Callback(value), Released(value)})

Drag or tap anywhere on the track. Values snap to Step (decimals work: 0.1, 0.25). Callback runs on every change; Released once when the finger or mouse lets go.

Tab:Slider({Title = "Field of view", Min = 50, Max = 110, Step = 1, Default = 70, Suffix = "°", Flag = "FOV",
    Callback = function(value) workspace.CurrentCamera.FieldOfView = value end})

Stepper

Stepper({Title, Description, Values | Min, Max, Step, Default, Format, Suffix, Flag, Callback(value)})
Tab:Stepper({Title = "Timer", Values = {10, 30, 60, 120, 300}, Default = 60,
    Format = function(s) return s < 60 and (s .. " s") or ((s / 60) .. " min") end})

Dropdown

Dropdown({Title, Description, Options, Default, Placeholder, Width, Flag, Callback(value)}) → {Set, Get, SetOptions(options, keepValue)}

Options are strings or {Value = any, Label = string}. With a Title the picker sits on a settings card; without one it fills the width. The list floats above the window, so a scrolling page never clips it, and scrolling or tapping outside closes it.

local mode = Tab:Dropdown({Title = "Mode", Options = {
    {Value = "fast", Label = "Fast"},
    {Value = "safe", Label = "Safe"},
}, Default = "safe", Flag = "Mode"})

mode:SetOptions({"Low", "High"}) -- keeps the value if it still exists

Input

Input({Title, Placeholder, Icon, Default, Button, ButtonIcon, Numeric, MaxLength, ClearOnFocus, Debounce, Changed(text), Flag, Callback(text, enterPressed)}) → {Set, Get, Focus, Box}
Tab:Input({
    Title = "Target", Placeholder = "Username or ID", Icon = "user",
    Button = true,                     -- arrow button on the right
    Changed = function(text) preview(text) end, -- while typing, after Debounce (0.25 s)
    Callback = function(text, enter) if enter then apply(text) end end,
})
Tab:Input({Placeholder = "Amount", Numeric = true, MaxLength = 6, Icon = false})

TextArea

TextArea({Text, Placeholder, Height, Editable, Code, Flag, Callback(text)}) → {Set, Get, Copy, Box} · alias: Code
local share = Tab:Code({Text = exportString(), Editable = false, Height = 70})
share:Copy() -- uses setclipboard when the executor has it

Keybind

Keybind({Title, Description, Default, Blocked, Flag, Callback(key), Changed(key)}) → {Set, Get}

Tap the button, then press a key; Esc cancels and the capture gives up after 10 s. Callback runs whenever the key is pressed in game (not while typing). Blocked lists key names that cannot be chosen.

Tab:Keybind({Title = "Dash", Default = Enum.KeyCode.Q, Flag = "DashKey", Blocked = {"W", "A", "S", "D"},
    Callback = function() dash() end})

Chips & Segmented

Chips({Options, Default, Small, Flag, Callback}) · Segmented({Options, Default, Height, Gap, Flag, Callback})
Tab:Chips({Options = {"All", "Hats", "Faces"}, Callback = filter})
Tab:Chips({Options = {"Shirts", "Pants"}, Small = true}) -- a lighter second row

Tab:Segmented({Options = {
    {Label = "Favorites", Icon = "star", Value = "fav"},
    {Label = "Recent", Icon = "history", Value = "recent"},
}, Callback = function(which) render(which) end})

Group, Columns, Collapsible

Group({Title, Gap, Padding}) → container · Columns({Gap}) → left, right · Collapsible({Title, OpenTitle, Open, Callback}) → container

Columns sit side by side and stack on narrow windows. A collapsible section starts folded so a page stays short; it adds SetOpen(bool) and IsOpen().

local status = Tab:Group({Title = "CYCLE STATUS"})
status:Paragraph("Waiting for the first switch.")

local left, right = Tab:Columns()
left:Toggle({Title = "Left option"})
right:Toggle({Title = "Right option"})

local advanced = Tab:Collapsible() -- "Show advanced options" / "Hide advanced options"
advanced:Toggle({Title = "Developer mode", Flag = "Dev"})

Guide & Empty

Guide({Title, OpenTitle, Lines, Note, Open}) · Empty({Title, Text, Icon})
Tab:Guide({Title = "How it works", Lines = {
    "1. Join a game and build your outfit.",
    "2. Come back here and tap Save.",
}, Note = "Saved items stay on this device."})

Tab:Empty({Title = "Nothing here yet", Text = "Items you add appear here.", Icon = "box"})

Row & List

List({Items, Reorderable, OnReorder(from, to, items), EmptyTitle, EmptyText, EmptyIcon, Empty}) → {Add, Remove, Set, Get, Clear, Refresh}

A row has a picture or icon, a title, a subtitle and buttons. Buttons with an icon and a title keep only the icon on narrow windows (Collapse = false keeps the title). Button callbacks receive (item, row). With Reorderable, rows get number badges: drag the number or the picture to move a row; a line shows where it lands and the page scrolls near its edges.

local queue = Tab:List({Reorderable = true, EmptyTitle = "Queue is empty",
    OnReorder = function(from, to, items) save(items) end})

queue:Add({
    Title = "Vega", Subtitle = "@vega", Current = true,
    Image = "rbxthumb://type=AvatarHeadShot&id=1&w=150&h=150",
    Buttons = {
        {Title = "Wear", Primary = true, Callback = function(item) wear(item) end},
        {Title = "Preview", Icon = "box", Callback = preview},
        {Icon = "x", Callback = function(item) queue:Remove(item) end},
    },
})

-- A single row anywhere:
local row = Tab:Row({Title = "Server", Subtitle = "12 players", Icon = "users"})
row:SetSelected(true)

Grid

Grid({Items, MinWidth = 150, CellHeight = 200, MinColumns = 2, MaxColumns = 6}) → {Add, Set, Clear, Cards}

Columns are sized in whole pixels so every row fits exactly. An item has Title, Caption, Image or Icon, Tag (a pill such as "NEW"), Checked (green check), Button (false, a title, or {Title, Primary, Icon, Callback(item, card)}) and OnClick for the picture. Cards have SetChecked, SetCaption and SetTitle.

local shop = Tab:Grid({})
shop:Add({Title = "Korblox Leg", Caption = "Body · R$ 17,000", Image = "rbxthumb://type=Asset&id=139607718&w=150&h=150",
    Button = {Title = "Try on", Callback = function(item, card) card:SetChecked(true) end}})

Preview (3D)

Preview({Title, Height, Hint, Icon, ControlsHint, Anchor}) → {SetModel(model, keepView), SetImage(image, message), Loading(on, message), Clear(message), SetTitle}

A ViewportFrame with a WorldModel: drag to rotate, scroll or use the buttons to zoom, reset with one tap. Pass a model you own (a clone); the preview destroys the previous one. Parts are anchored unless Anchor = false.

local view = Tab:Preview({Title = "YOUR CHARACTER", Hint = "Tap Load."})
view:Loading(true, "Building the preview…")
local character = game.Players.LocalPlayer.Character
character.Archivable = true
view:SetModel(character:Clone())

Strip

Strip({Title, Icon, Hint, Empty, Items, Buttons, ScrollToEnd}) → {Set(items), SetHint(text), Buttons}
local history = Tab:Strip({Title = "Timeline", Buttons = {
    {Title = "Undo", Icon = "undo", Callback = undo},
    {Title = "Restore", Icon = "reset", Callback = restore},
}})
history:Set({
    {Label = "Original", Image = headshot(you), Callback = goBack},
    {Label = "Now", Image = headshot(target), Current = true},
})

Link, Profile, Image

Link shows an underlined URL with a Copy link button; without clipboard access the URL becomes selectable text. Profile shows a portrait (an image, a web image downloaded with ImageUrl, or an initial), a caption, a name, a handle, a rich-text bio and an optional link. Image is a rounded picture with an optional caption.

Tab:Link({Title = "Discord", Subtitle = "Join the community", Icon = "discord", Url = "https://discord.gg/8bxXcn2Vfb"})
Tab:Profile({Caption = "GITHUB PROFILE", Title = "lyraEz", Handle = "@lyraEz",
    ImageUrl = "https://github.com/lyraEz.png?size=256", Url = "https://github.com/lyraEz",
    Bio = "The moon reminds me that even in darkness, there's always a <b>beautiful light</b>"})
Tab:Image({Image = "rbxassetid://123", Height = 140, Caption = "Season 2"})

Themes

Built in: Lavender (the default), Midnight, Rose, Mint and Ember. A theme is a table of Color3 values with these keys: Base (window), Card, Raised (buttons and inputs), Line, Accent, AccentDark, Text, Muted, Green, Red, Gold, Link, SheenA and SheenB (the window's sheen). Missing keys come from Lavender, or from the theme named in Extends.

Every instance stores the theme key of its colours, so SetTheme repaints the live window without rebuilding anything.

Lyra.AddTheme("Aurora", {Base = Color3.fromRGB(12, 22, 26), Accent = Color3.fromRGB(120, 230, 210)})
Window:SetTheme("Aurora")
Window:SetTheme({Extends = "Midnight", Accent = Color3.fromRGB(255, 120, 160)}) -- one-off palette

Icons

Pass any of these names as Icon. Aliases also work: avatar, players, collection, settings, creator, code, close, expand, undo, arrow, reset, trash, share, edit, game, refresh, warning, success, error, hidden, sliders. An rbxassetid:// string or a number uses your own image, tinted with the theme.

Library functions

FunctionWhat it does
Lyra:CreateWindow(options)Creates a window. Lyra.new is the same.
Lyra.AddTheme(name, colors)Registers a theme (missing keys from Lavender).
Lyra.Themes · Lyra.ThemeNamesEvery theme by name, and the names in order.
Lyra.GetWindow(name)The live window with that name, if any.
Lyra.DestroyAll()Destroys every Lyra window.
Lyra.WebImage(url, file)Downloads a PNG or JPEG and returns an asset id (needs request or HttpGet, writefile and getcustomasset). Yields.
Lyra.ToastThe notifier on its own: Lyra.Toast.new({Parent = frame}).
Lyra.Font · Lyra.VersionThe fonts in use (Builder Sans, falling back to Gotham) and the version string.

Testing

The repository runs the library outside Roblox against a small stand-in of the Roblox API (tests/mock.lua). It builds the showcase, clicks through every element, resizes, drags, closes and reopens the window, switches themes and checks the saved config, and fails on any misspelled property, wrong value type or error inside a callback.

tests/run.sh path/to/luau