Audio and Interface
UIService
A declarative, reactive UI framework for Roblox GUIs with components, themes and styles, animation, screens and dialogs, layers, focus and effects.
Getting it
local UI = Engine:GetService("UIService")Overview
UIService lets you describe interfaces as nested tables instead of creating and wiring instances one by one. Properties can be plain values or reactive values (from State); when a reactive value changes, every property bound to it updates by itself. You never write "find the label and set its text" again.
GUIs are shown on the client, so UIService is used from LocalScripts.
local Players = game:GetService("Players")
local Engine = require(game:GetService("ReplicatedStorage"):WaitForChild("Engine"))
local UI = Engine:GetService("UIService")
local clicks = UI:Value(0)
local Counter = UI:Component(function(props)
return UI.Frame {
Size = UDim2.fromOffset(260, 120),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundColor3 = UI:Token("Colors.Surface"),
Corner = 10,
Padding = 12,
Layout = { Direction = "Vertical", Gap = 8 },
UI.TextLabel {
Size = UDim2.new(1, 0, 0, 28),
Text = UI:Format("%s clicked %d times", props.Name, clicks),
TextColor3 = UI:Token("Colors.Text"),
FontFace = UI:Token("Fonts.Body"),
},
UI.TextButton {
Size = UDim2.new(1, 0, 0, 40),
Text = "Click me",
BackgroundColor3 = UI:Token("Colors.Primary"),
TextColor3 = UI:Token("Colors.TextOnPrimary"),
Corner = 8,
Hover = { BackgroundTransparency = 0.1 },
Press = { Scale = 0.96 },
OnActivated = function()
clicks:Update(function(n)
return n + 1
end)
end,
},
}
end, "Counter")
UI:Mount(UI.ScreenGui { Counter { Name = Players.LocalPlayer.DisplayName } }, Players.LocalPlayer.PlayerGui)Elements
Creating instances
UI.Frame { ...props, ...children }
UI:New("Frame") { ... }Any creatable Roblox class is available as UI.ClassName. Inside the props table:
| Key | Meaning |
|---|---|
| A property name | Sets the property. The value can be plain or reactive. Misspelled names raise a clear error. |
| Array entries | Children, in order. Children = { ... } works too. Children get a LayoutOrder from their position unless they set one. |
OnEventName | Connects to an event, e.g. OnActivated, OnMouseEnter, OnFocusLost. |
ChangedProperty | Called with the new value when a property changes, e.g. ChangedText = function(text, instance) end. |
Ref | A UI:Ref() value, or a function, that receives the created instance. |
Tag | A CollectionService tag or list of tags. |
Attributes | { name = value }, reactive values allowed. |
Style, Variant | Apply defined styles, see Themes and styles. |
OnMount, OnUnmount | Called with the instance when it is mounted or cleaned up. |
An element never sets Parent itself; nest it, or use Mount or Portal.
Modifiers
Modifiers add the usual helper instances for you:
| Key | Creates | Value |
|---|---|---|
Corner | UICorner | Pixels or a UDim. |
Padding | UIPadding | Pixels, or { Top, Bottom, Left, Right, X, Y }. |
Stroke | UIStroke | Thickness, a Color3, or a table of UIStroke properties. |
Gradient | UIGradient | A colour, a ColorSequence, or a table of properties. |
Layout | UIListLayout / UIGridLayout | "Vertical", "Horizontal", or { Direction, Gap, ... } with Direction = "Grid" for a grid. |
AspectRatio | UIAspectRatioConstraint | A ratio or a table. |
Scale | UIScale | A number. |
Flex | UIFlexItem | A flex mode name or a table. |
MinSize, MaxSize | UISizeConstraint | Vector2. |
Glow, Shimmer, Ripple | Animated effects | true or a config table. Ripple plays on click. |
Interaction states
Hover, Press and Enter take a table of property values to animate to. Enter (or a preset: "Fade", "Pop", "Grow", "Spin", "Drop") animates from those values when the element appears. Motion sets the animation (a spring config or tween info). Scale and, on a CanvasGroup, Transparency are handled specially, so they can be animated on anything.
UI.ImageButton {
Image = "rbxassetid://123",
Enter = "Pop",
Hover = { Scale = 1.05 },
Press = { Scale = 0.95, ImageTransparency = 0.2 },
}Components
UI:Component(render: (props) -> element, name: string?): (props?) -> elementA reusable function that builds elements from props. Components can use reactive values, contexts and effects; everything they create is cleaned up when they unmount.
Mount / Apply
UI:Mount(element, parent: Instance): Mounted
UI:Apply(instance: Instance, props): MountedMount builds an element tree under parent. Apply binds props to an instance you already have (for example one placed in Studio). Both return a handle with Instance, Instances, Destroyed and :Destroy(), which removes the instances and disconnects everything.
Reactivity
| Method | Description |
|---|---|
UI:Value(initial, equals?) | A writable reactive value. |
UI:Computed(fn, equals?) | A value derived from other values. |
UI:Effect(fn) | Runs fn now and whenever what it read changes. |
UI:Observe(source, fn) | Calls fn(new, old) on changes; returns a function that stops. |
UI:Map(source, fn) | Computed(function() return fn(source:Get()) end). |
UI:Format(format, ...) | A computed string.format whose arguments may be reactive. |
UI:Batch(fn), UI:Peek(value) | See State. |
UI:TwoWay(value) | Bind a property in both directions, e.g. Text = UI:TwoWay(name) on a TextBox. |
UI:Ref() | A value that receives an instance through the Ref key. |
Control flow
| Method | Description |
|---|---|
UI:Show(condition, thenRender, elseRender?) | Renders one branch, swapping when the reactive condition changes. |
UI:Switch(source, cases, default?) | Renders cases[value]. |
UI:For(source, render(item, index), keyOf?) | Renders a list from a reactive array or dictionary. With keyOf, items that move keep their instances. |
UI:Await(promise, { Pending, Resolved, Rejected }) | Renders according to a Promise's state. Handlers get (value, error). |
UI:Rerender(sources, render) | Rebuilds a subtree whenever any of the sources change. |
UI:Portal(target, children) | Renders children under another instance, such as a layer. |
UI:Context(default, name?) | Shares a value with every component below: context:Provide(value, children) and context:Use(). |
UI.ScrollingFrame {
Layout = "Vertical",
UI:For(inventory, function(item)
return ItemRow { Item = item }
end, function(item)
return item.Id
end),
}Animation
Animated values can be numbers, Vector2, Vector3, CFrame, Color3, UDim, UDim2 or Rect.
| Method | Description |
|---|---|
UI:Spring(source, config?) | A value that follows source with a Spring. config is a speed, a preset name or { speed, damper }. |
UI:Tween(source, info?) | A value that tweens to each new value of source. info is a duration, a TweenInfo, or { Seconds, EasingStyle, EasingDirection }. |
UI:Loop(config?) | A value that animates between From and To forever. Mode = "Restart" jumps back; otherwise it ping-pongs. |
UI:Delay(source, seconds) | Follows source after a delay. |
UI:Keyframes(driver, frames) | Maps a 0-1 driver through keyframes { At, Value }. |
UI:Number(source, config?) | Animated, formatted number text: { Seconds?, Spring?, Decimals?, Prefix?, Suffix?, Separator?, Compact?, Format? }. Compact shows 12.5K, 3.1M. |
UI:FormatNumber(value, config?) | The same formatting, once. |
UI:States(current, states, motion?) | Animated values for named states, e.g. Idle/Hover/Selected. |
UI:Sequence(steps, { Loop? }?) | Runs steps in order: { Set, To, Seconds }, { Wait }, { Call }. Returns { Playing, :Cancel() }. |
local coins = UI:Value(0)
UI.TextLabel {
Text = UI:Number(coins, { Prefix = "$", Separator = ",", Seconds = 0.6 }),
}Themes and styles
A theme is a table of design tokens: colours, fonts, text sizes, corner radius, spacing. The default theme comes from configuration; UI:Token("Colors.Primary") returns a reactive value that updates when the theme changes, so switching to a light theme restyles everything.
| Member | Description |
|---|---|
UI:Token(path) | A reactive token, by dotted path. |
UI:SetTheme(tokens) | Merges tokens into the current theme. |
UI.Theme | The whole token table; setting it replaces the theme. |
UI.ThemeChanged | Fires with the new tokens. |
UI:DefineStyle(name, props) | A named set of props, with optional Variants = { [name] = props }. |
UI:GetStyle(name) |
UI:DefineStyle("Button", {
BackgroundColor3 = UI:Token("Colors.SurfaceRaised"),
TextColor3 = UI:Token("Colors.Text"),
Corner = 8,
Variants = {
Primary = { BackgroundColor3 = UI:Token("Colors.Primary") },
Danger = { BackgroundColor3 = UI:Token("Colors.Danger") },
},
})
UI.TextButton { Style = "Button", Variant = "Danger", Text = "Delete" }Style can be one name or a list; later styles and the element's own props win. Variant can be reactive.
Screens and dialogs
Screens are full-screen views (menus, shops, settings), each in its own ScreenGui, managed as a stack.
| Method | Description |
|---|---|
UI:RegisterScreen(name, builder(params)) | Registers a screen. |
UI:Push(name, params?, transition?) | Opens a screen on top. |
UI:Pop() | Closes the top screen. |
UI:Replace(name, params?, transition?) | Swaps the top screen. |
UI:Close(name), UI:CloseAll() | |
UI:GetStack(), UI:IsOpen(name) | |
UI:SetTransition(name, { In, Out }) | Adds a custom transition. Built in: None, Fade, Scale, SlideUp, SlideDown, SlideLeft, SlideRight. |
UI:BindToggle(name, keys), UI:UnbindToggle(name) | Opens and closes a screen with a key or a list of keys (anything ContextActionService accepts), e.g. Enum.KeyCode.M for a map. |
UI.ScreenChanged: Signal<(top?, stack)> |
Every screen's ScreenGui takes ScreenResetOnSpawn and ScreenIgnoreGuiInset from configuration. Set ScreenInsets to an Enum.ScreenInsets name, such as "DeviceSafeInsets", to keep screens clear of notches and rounded corners; left empty, screens follow ScreenIgnoreGuiInset.
DialogAsync
UI:DialogAsync(config: { Title?, Body?, Buttons?, Dismissable?, Width? }): Promise<any>Shows a themed dialog and resolves with the Value of the button pressed. Buttons are { Text, Value?, Variant? } (Variant = "Primary" highlights one).
UI:DialogAsync({
Title = "Sell item?",
Body = "You will get 250 coins.",
Buttons = {
{ Text = "Cancel", Value = false },
{ Text = "Sell", Value = true, Variant = "Primary" },
},
}):andThen(function(confirmed)
if confirmed then
sellItem()
end
end)Layers
Layers are stacking levels above the screens: Background, World, Content, Overlay, Popup, Modal, Toast, Tooltip and Debug by default (see Layers in configuration). A modal layer blocks input to everything below it while it holds anything, optionally with a dimmed scrim that can dismiss it.
| Method | Description |
|---|---|
UI:Layer(name): Layer | A layer: :Mount(element), :Open(element), :Clear(), plus Container, IsOpen, Visible, Interactable and the Dismissed signal. |
UI:DefineLayer(name, config) | Adds a layer: { Order, Class?, Modal?, Scrim?, ScrimTransparency?, ScrimColor?, Dismiss?, ScreenInsets?, ... }. ScreenInsets is an Enum.ScreenInsets or its name, such as "DeviceSafeInsets", for the layer's ScreenGui. |
UI:Popup(element), UI:Toast(element), UI:Tooltip(element) | Mount into the matching layer. |
UI:ClearLayer(name), UI:ClearLayers() | |
UI:IsLayerOpen(name), UI:IsLayerBlocked(name) | |
UI:LayerNames(), UI.LayerChanged: Signal<(name, open)> |
Focus
For gamepad and keyboard navigation UIService moves GuiService.SelectedObject for you (by default only while a gamepad is in use).
| Member | Description |
|---|---|
UI:SetFocus(instance?), UI:GetFocus() | |
UI.FocusEnabled | Turn the focus system on or off. |
Effects
| Method | Description |
|---|---|
UI:Flash(instance, { Color?, Seconds?, Transparency? }?) | A quick colour flash, e.g. on taking damage. |
UI:Shake(instance, { Seconds?, Degrees?, Scale?, Shakes? }?) | A small shake (rotation, so layouts are not disturbed). |
UI:Burst(instance, { Color?, Count?, Seconds?, Distance?, Size? }?) | Particles bursting out of an element. |
UI:Typewriter(textObject, { Text?, CharactersPerSecond?, OnFinished? }?) | Reveals text letter by letter. |
UI:Blur(enabled, size?) | Blurs the 3D world behind the UI. |
UI:Shadow({ Spread?, Steps?, Transparency?, Color?, Corner?, Offset? }) | A soft drop shadow element to place behind a frame. |
Configuration
DefaultConfiguration/Services/UIService38 settings| Setting | Default | What it does |
|---|---|---|
| AutoLayoutOrder | true | Assign LayoutOrder from a child's position in its parent's child list when the child sets none. |
| ApplyClassDefaults | true | Apply ClassDefaults to every created Instance before styles and props; false gives Roblox's raw defaults. |
| ClassDefaults | { { Class = "GuiObject", Props = { BorderSizePixel = 0 } }, { Class = "TextLabel", Props = { BackgroundTransparency = 1 } }, { Class = "ImageLabel", Props = { BackgroundTransparency = 1 } }, { Class = "TextButton", Props = { AutoButtonColor = false } }, { Class = "ImageButton", Props = { AutoButtonColor = false, BackgroundTransparency = 1 } }, { Class = "ScrollingFrame", Props = { ScrollBarThickness = 4, ScrollBarImageTransparency = 0.5 } }, } | Applied in array order to every created Instance that :IsA the listed class. |
| DefaultSpringSpeed | 24 | Speed :Spring() uses when none is passed; same meaning as EnginePackages.Spring's speed. |
| DefaultSpringDamper | 1 | Damper :Spring() uses when none is passed; 1 is critically damped (no overshoot). |
| DefaultTweenSeconds | 0.18 | Duration :Tween() uses when none is passed. |
| DefaultTweenEasingStyle | Enum.EasingStyle.Quad | EasingStyle :Tween() uses when none is passed. |
| DefaultTweenEasingDirection | Enum.EasingDirection.Out | EasingDirection :Tween() uses when none is passed. |
| RippleSeconds | 0.45 | Duration of a Ripple. |
| RippleTransparency | 0.75 | Transparency a Ripple starts at. |
| FlashSeconds | 0.25 | Duration of a Flash. |
| ShakeSeconds | 0.35 | Duration of a Shake. |
| ShakeDegrees | 3 | Rotation of a Shake; it rotates rather than moves so a layout can't fight it. |
| BurstSeconds | 0.6 | Duration of a Burst. |
| BurstCount | 12 | Particles a Burst spawns. |
| TypewriterCharactersPerSecond | 45 | Reveal speed of a Typewriter. |
| ShimmerSeconds | 1.2 | Duration of one Shimmer sweep. |
| GlowSeconds | 1.6 | Duration of one Glow pulse. |
| ShadowSteps | 5 | Stacked rounded frames a Shadow is built from; more is smoother but more instances. |
| ShadowSpread | 12 | Pixels a Shadow extends past its element. |
| ShadowTransparency | 0.82 | Transparency of the innermost Shadow step; every step outward fades further. |
| ShadowColor | Color3.fromRGB(0, 0, 0) | Color of a Shadow. |
| BlurSize | 18 | Lighting BlurEffect size :Blur() applies. |
| DefaultTheme | { Colors = { Background = Color3.fromRGB(16, 16, 20), Surface = Color3.fromRGB(26, 26, 32), SurfaceRaised = Color3.fromRGB(38, 38, 46), Border = Color3.fromRGB(54, 54, 64), Overlay = Color3.fromRGB(0, 0, 0), Primary = Color3.fromRGB(94, 134, 255), Success = Color3.fromRGB(76, 195, 138), Warning = Color3.fromRGB(232, 178, 74), Danger = Color3.fromRGB(228, 92, 92), Text = Color3.fromRGB(238, 238, 244), TextMuted = Color3.fromRGB(148, 148, 162), TextOnPrimary = Color3.fromRGB(255, 255, 255), }, Fonts = { Body = Font.fromEnum(Enum.Font.Gotham), Heading = Font.fromEnum(Enum.Font.GothamBold), Mono = Font.fromEnum(Enum.Font.Code), }, TextSize = { Small = 12, Body = 14, Heading = 20, Title = 28, }, Corner = 8, Padding = 12, Gap = 8, Stroke = 1, OverlayTransparency = 0.45, } | Token table :Token(path) reads and :SetTheme() merges into. |
| LayerDefaultClass | "Frame" | Root class of a layer; "CanvasGroup" allows fading a whole layer at once. |
| LayerScrimColor | Color3.fromRGB(0, 0, 0) | Color of a layer's scrim. |
| Layers | { { Name = "Background", Order = 10 }, { Name = "World", Order = 50 }, { Name = "Content", Order = 100 }, { Name = "Overlay", Order = 200 }, { Name = "Popup", Order = 300, Modal = true, Scrim = true, Dismiss = true }, { Name = "Modal", Order = 400, Modal = true, Scrim = true, ScrimTransparency = 0.45, Class = "CanvasGroup", }, { Name = "Toast", Order = 500 }, { Name = "Tooltip", Order = 600 }, { Name = "Debug", Order = 900 }, } | Stacking contexts :Layer(name) opens into; Modal blocks input to every lower layer while it holds anything. |
| ScreenBaseDisplayOrder | 100 | DisplayOrder of the bottom screen; each one above adds 1. |
| ScreenRootClass | "CanvasGroup" | Root class of a screen; CanvasGroup lets a fade move the whole screen's alpha, "Frame" opts out. |
| ScreenResetOnSpawn | false | ResetOnSpawn of every screen's ScreenGui. |
| ScreenIgnoreGuiInset | true | IgnoreGuiInset of every screen's ScreenGui. |
| ScreenInsets | "" | Enum.ScreenInsets name every screen's ScreenGui uses; empty follows ScreenIgnoreGuiInset. |
| DefaultTransition | "Fade" | Transition :Push()/:Pop() use when none is passed: None/Fade/Scale/SlideUp/SlideDown/SlideLeft/SlideRight. |
| DefaultTransitionSeconds | 0.18 | Duration of a screen transition when none is passed. |
| DialogTransition | "Scale" | Transition :DialogAsync() uses. |
| DialogWidth | 360 | Pixel width of a built-in dialog. |
| FocusEnabled | true | Whether the focus system moves GuiService.SelectedObject at all. |
| FocusOnlyForGamepad | true | Whether focus only moves while the player is actually on a gamepad. |
Types
export type Value
export type Computed
export type Effect
export type Props
export type Element
export type Mounted
export type Context
export type Tokens
export type Transition
export type DialogConfig