Skip to content
Engine

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.

ServiceShared

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.

A counter
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:

KeyMeaning
A property nameSets the property. The value can be plain or reactive. Misspelled names raise a clear error.
Array entriesChildren, in order. Children = { ... } works too. Children get a LayoutOrder from their position unless they set one.
OnEventNameConnects to an event, e.g. OnActivated, OnMouseEnter, OnFocusLost.
ChangedPropertyCalled with the new value when a property changes, e.g. ChangedText = function(text, instance) end.
RefA UI:Ref() value, or a function, that receives the created instance.
TagA CollectionService tag or list of tags.
Attributes{ name = value }, reactive values allowed.
Style, VariantApply defined styles, see Themes and styles.
OnMount, OnUnmountCalled 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:

KeyCreatesValue
CornerUICornerPixels or a UDim.
PaddingUIPaddingPixels, or { Top, Bottom, Left, Right, X, Y }.
StrokeUIStrokeThickness, a Color3, or a table of UIStroke properties.
GradientUIGradientA colour, a ColorSequence, or a table of properties.
LayoutUIListLayout / UIGridLayout"Vertical", "Horizontal", or { Direction, Gap, ... } with Direction = "Grid" for a grid.
AspectRatioUIAspectRatioConstraintA ratio or a table.
ScaleUIScaleA number.
FlexUIFlexItemA flex mode name or a table.
MinSize, MaxSizeUISizeConstraintVector2.
Glow, Shimmer, RippleAnimated effectstrue 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?) -> element

A 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): Mounted

Mount 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

MethodDescription
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

MethodDescription
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.

MethodDescription
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.

MemberDescription
UI:Token(path)A reactive token, by dotted path.
UI:SetTheme(tokens)Merges tokens into the current theme.
UI.ThemeThe whole token table; setting it replaces the theme.
UI.ThemeChangedFires 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.

MethodDescription
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.

MethodDescription
UI:Layer(name): LayerA 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).

MemberDescription
UI:SetFocus(instance?), UI:GetFocus()
UI.FocusEnabledTurn the focus system on or off.

Effects

MethodDescription
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

Defaults from DefaultConfiguration/Services/UIService38 settings
SettingDefaultWhat it does
AutoLayoutOrdertrueAssign LayoutOrder from a child's position in its parent's child list when the child sets none.
ApplyClassDefaultstrueApply 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.
DefaultSpringSpeed24Speed :Spring() uses when none is passed; same meaning as EnginePackages.Spring's speed.
DefaultSpringDamper1Damper :Spring() uses when none is passed; 1 is critically damped (no overshoot).
DefaultTweenSeconds0.18Duration :Tween() uses when none is passed.
DefaultTweenEasingStyleEnum.EasingStyle.QuadEasingStyle :Tween() uses when none is passed.
DefaultTweenEasingDirectionEnum.EasingDirection.OutEasingDirection :Tween() uses when none is passed.
RippleSeconds0.45Duration of a Ripple.
RippleTransparency0.75Transparency a Ripple starts at.
FlashSeconds0.25Duration of a Flash.
ShakeSeconds0.35Duration of a Shake.
ShakeDegrees3Rotation of a Shake; it rotates rather than moves so a layout can't fight it.
BurstSeconds0.6Duration of a Burst.
BurstCount12Particles a Burst spawns.
TypewriterCharactersPerSecond45Reveal speed of a Typewriter.
ShimmerSeconds1.2Duration of one Shimmer sweep.
GlowSeconds1.6Duration of one Glow pulse.
ShadowSteps5Stacked rounded frames a Shadow is built from; more is smoother but more instances.
ShadowSpread12Pixels a Shadow extends past its element.
ShadowTransparency0.82Transparency of the innermost Shadow step; every step outward fades further.
ShadowColorColor3.fromRGB(0, 0, 0)Color of a Shadow.
BlurSize18Lighting 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.
LayerScrimColorColor3.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.
ScreenBaseDisplayOrder100DisplayOrder 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.
ScreenResetOnSpawnfalseResetOnSpawn of every screen's ScreenGui.
ScreenIgnoreGuiInsettrueIgnoreGuiInset 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.
DefaultTransitionSeconds0.18Duration of a screen transition when none is passed.
DialogTransition"Scale"Transition :DialogAsync() uses.
DialogWidth360Pixel width of a built-in dialog.
FocusEnabledtrueWhether the focus system moves GuiService.SelectedObject at all.
FocusOnlyForGamepadtrueWhether 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