Making UI Interactive

*เนื้อหานี้จะพร้อมใช้งานในภาษาที่คุณเลือกในเร็วๆ นี้

A HUD (Heads-Up Display) in your experience commonly displays non‑interactive UI elements such as the health meter demonstrated in Creating HUD Meters. Atop this, almost every experience requires interactive UI such as buttons that respond to player activation, animate when activated, tween in/out menus with other interactive controls, etc.

In-game view showing a settings button and settings menu with volume sliders.

Using UI Fundamentals - HUD Meter as a starting place and UI Fundamentals - Interactivity as a finished reference place, this tutorial demonstrates:

  • How to position a settings button along the top screen edge.
  • Design of a settings menu containing interactive draggable sliders.
  • Usage of a ModuleScripts to form "controller" modules for extensible control of UI objects with stateful declarations.
  • How to connect buttons to player activation to toggle in/out the settings menu.
  • How to connect draggable UI sliders to adjust the volume of sound effects and background ambience separately.

Using the Device Emulator

As noted in Creating HUD Meters, phones and tablets have the least amount of screen space, so it's important that your UI elements fit on smaller screens and that they're clearly visible to players. If you haven't done so already, enable the Device Emulator in Studio:

  1. Open the UI Fundamentals - HUD Meter template in Studio.

  2. From the Test tab, toggle on the Device tool.

    Device button indicated in Test tab
  3. From the bar directly above the main viewport, select a phone emulation such as iPhone X or Samsung Galaxy A51. Then, set the view size to Fit to Window to utilize the maximum space in Studio.

    Device Emulator settings options indicated at top of viewport window.

Create the Settings Button

GuiButtons are interactive user interface elements with built‑in functionality such as the multi‑platform Activated event which fires when the button is clicked or tapped. The GuiButton class extends to two variations, TextButton and ImageButton, and this tutorial uses a "gear" shaped ImageButton to toggle the settings menu open or closed.

Roblox component used for the settings button.

To construct the settings button:

  1. In the Explorer window, locate HUDContainer inside StarterGui.

    Explorer window showing the HUDContainer selected.
  2. Insert an ImageButton into HUDContainer and rename it to SettingsButton.

    Explorer window showing new ImageButton inserted and renamed to SettingsButton. New ImageButton in viewport.
  3. With the new button selected, set the following in the Properties window:

    • AnchorPoint = 0.5, 0.25 (horizontal center and vertical upper quarter)
    • BackgroundTransparency = 1 (fully transparent)
    • Position = 0.5, 0, 0, 0 (top-center positioning)
    • Size = 0.1, 0, 0.1, 0 (10% of screen height once aspect ratio is constrained)
    • Image = rbxassetid://104919049969988 (asset ID of "gear" symbol)
  4. Insert a UIAspectRatioConstraint into the button with default properties (1:1 width to height).

    Explorer window showing a new UIAspectRatioConstraint inserted into the SettingsButton button.
  5. To limit the button's maximum pixel height, insert a UISizeConstraint and set its MaxSize to inf, 44.

    Explorer window showing a new UISizeConstraint inserted into the SettingsButton button.
    Properties window showing expected values for the new UISizeConstraint.
    Final settings button in top-center of the screen.

Create the Settings Menu

The new settings button will be scripted to toggle a settings menu open and closed, giving players on‑demand access to settings or other information. In this tutorial, the menu will contain interactive sliders to adjust the volume levels of background audio and sound effects independently.

Basic components used for the settings menu.

Create the Parent Frame

As noted in Creating HUD Meters, a Frame serves as a container for other UI objects. The entire settings menu will be constructed with a single parent frame, making it simple to manage as a stateful object that reacts to input differently depending on the current state.

  1. Insert a Frame into HUDContainer and rename it SettingsMenu.

    Explorer window showing new frame inserted and renamed to SettingsMenu.
  2. With SettingsMenu selected, set the following properties:

    Frame repositioned, resized, and styled with transparent navy blue background.
  3. Insert a UIAspectRatioConstraint into SettingsMenu and set its AspectRatio property to 2.5. (2.5:1 width to height).

    Explorer window showing a new UISizeConstraint inserted into the SettingsMenu frame.
    Properties window showing expected values for the new UISizeConstraint.
  4. Insert a UICorner modifier into SettingsMenu and set its CornerRadius to 0.1, 0.

    Explorer window showing a new UICorner inserted into the SettingsMenu frame.
    Properties window showing expected values for the new UICorner modifier.
  5. To constrain the pixel width and height of SettingsMenu, insert a UISizeConstraint. Set its MaxSize to 800, inf and its MinSize to 350, 0.

    Explorer window showing a new UISizeConstraint inserted into the SettingsMenu frame.
    Properties window showing expected values for the new UISizeConstraint.
    Frame styled with rounded corners.

Construct a Slider

To allow players to adjust volume levels, the settings menu will contain two draggable slider widgets powered by UIDragDetector, a convenient object that facilitates interaction with 2D user interface elements.


To create a parent container for the first slider:

  1. Insert a new Frame into the SettingsMenu container and rename it EffectsVolumeSlider.

    Explorer window showing a new frame inserted into the SettingsMenu frame, renamed to EffectsVolumeSlider.
  2. With the EffectsVolumeSlider frame selected, set the following properties:

    • AnchorPoint = 0.5 (center anchor)

    • BackgroundTransparency = 1 (fully transparent)

    • Position = 0.5, 0, 0.35, 0 (horizontal center and 35% from container's top)

    • Size = 0.8, 0, 0.1, 0 (80% of container's width and 10% of its height)

      Empty frame inserted to contain slider elements.
  3. Insert a new UIListLayout into EffectsVolumeSlider. This layout modifier is a powerful way to auto‑arrange sibling GuiObjects into horizontal rows or vertical columns within their parent container, including the ability to apply flex concepts.

    Explorer window showing a new UIListLayout inserted into the EffectsVolumeSlider frame.
  4. Set the following properties for the new UIListLayout:

Slider Icon

A simple icon including an audio note and "burst" symbol helps players identify the slider's purpose when they open the settings menu.

Icon indicating purpose of slider in settings menu.
  1. Insert an ImageLabel into the EffectsVolumeSlider frame and rename it to Icon. This icon will help players understand what in‑experience aspect the slider controls.

    Explorer window showing an ImageLabel inserted into the EffectsVolumeSlider frame.
  2. With Icon selected, set the following properties:

    • BackgroundTransparency = 1 (fully transparent)
    • Size = 2.5, 0, 2.5, 0 (250% height of parent frame once aspect ratio is constrained)
    • Image = rbxassetid://90019827067389 (asset ID of audio note and "burst" symbol)
  3. Insert a UIAspectRatioConstraint into Icon and leave its properties as the defaults (1:1 width to height).

    Explorer window showing a UIAspectRatioConstraint inserted into the ImageLabel. Finalized icon in the settings menu.

Range Frame

Directly to the right of the icon, the interactive portion of the slider should be contained within another Frame.

Target size and position of the interactive portion of the slider.
  1. Insert a new Frame into the EffectsVolumeSlider frame and rename it SliderFrame. Note that it must be a direct sibling of the UIListLayout layout modifier.

    Explorer window showing a child Frame inserted into the EffectsVolumeSlider frame.
  2. With SliderFrame selected, set the following properties:

  3. Insert the following modifiers into SliderFrame:

    Slider frame positioned to the right of the icon.

Interactive Handle

With the slider container constructed, you can now create a draggable handle for players to interact with during gameplay.

Interactive slider handle positioned within range frame.
  1. Insert a new Frame into the SliderFrame container and rename it to Handle.

    Explorer window showing a child Frame inserted into the SliderFrame frame and renamed to Handle.
  2. With Handle selected, set the following properties:

    • AnchorPoint = 0.5 (center anchor)
    • Position = 0.5, 0, 0.5, 0 (horizontal and vertical center of parent frame)
    • Size = 1.2, 0, 1.2, 0 (120% height of parent frame once aspect ratio is constrained)
    • ZIndex = 3 (layer handle visually in front of other slider elements)
  3. Insert the following modifiers into Handle:

  4. Insert a UIDragDetector object into Handle. This convenient object facilitates and encourages interaction with 2D user interface elements.

    Explorer window showing a UIDragDetector inserted into the Handle frame.
  5. Set the following properties for the new UIDragDetector:

  6. To ensure the handle's linear drag range is limited to its container, link its BoundingUI property to the SliderFrame container:

    1. Select the UIDragDetector.
    2. Click its BoundingUI property in the Properties window.
    3. Back in the Explorer window, click the handle's parent SliderFrame.
    Diagram showing process of linking the detector's BoundingUI property to the SliderFrame container.

    The BoundingUI property link now reflects the SliderFrame container:

    Properties window showing the detector's BoundingUI property linked to the SliderFrame container.

If you playtest the experience now, you'll be able to drag the handle left and right within its parent container:

Inner Fill

To more clearly indicate that the slider controls a 0% to 100% range, you can add an inner fill to the left side of the container which will sync with the handle's variable position.

Inner fill portion of slider inserted into SliderFrame.
  1. Insert a new Frame into the SliderFrame container and rename it to InnerFill.

    Explorer window showing a child Frame inserted into the SliderFrame frame and renamed to InnerFill.
  2. Set the following properties for the InnerFill frame:

  3. To match the "pill" shape of the parent SliderFrame container, insert a UICorner modifier and set its CornerRadius property to 0.5, 0.

    Explorer window showing a UICorner modifier inserted into the InnerFill frame.
    Properties window showing expected values for the new UICorner.
    InnerFill frame styled with blue background and rounded corners.

Duplicate the Slider

With the first slider constructed, you can easily duplicate it and modify some visual aspects to indicate another purpose, in this case the volume level of background audio symbolized by an icon of musical notes.

Target position of the background audio slider.
  1. Select the completed EffectsVolumeSlider object.

    Explorer window showing the completed EffectsVolumeSlider selected.
  2. Duplicate it (CtrlD or D) and then rename the duplicate to BackgroundVolumeSlider.

    Explorer window showing the duplicated slider renamed to BackgroundVolumeSlider.
  3. Change the duplicate's Position to 0.5, 0, 0.7, 0 to move it below the first slider.

    BackgroundVolumeSlider positioned below EffectsVolumeSlider.
  4. Expand the top-level branch of BackgroundVolumeSlider, select the Icon image label, and change its Image property to rbxassetid://101125859760167 (asset ID of symbol with musical notes).

    Explorer window showing the Icon label within BackgroundVolumeSlider.
    Properties window showing expected values for the Icon label.
    BackgroundVolumeSlider with modified icon image.
  5. Expand the SliderFrameHandle branch, select the UIStroke modifier within, and set its Color property to [255, 0, 125] (magenta).

    Explorer window showing the UIStroke modifier within the handle of BackgroundVolumeSlider.
    Properties window showing expected values for the UIStroke modifier.
    BackgroundVolumeSlider with modified icon image.
  6. Select the InnerFill frame within SliderFrame and change its BackgroundColor3 property to the same magenta color ([255, 0, 125]).

    Explorer window showing the InnerFill frame within the SliderFrame container of BackgroundVolumeSlider.
    Properties window showing expected values for the InnerFill frame.
    BackgroundVolumeSlider with modified InnerFill color.

Create the Close Button

The final element of the settings menu is a close button which provides players an additional input to close the menu (the SettingsButton in the top‑center will serve the same purpose).

Settings menu with close button indicated in upper-right corner.
  1. Insert a new ImageButton into the SettingsMenu container and rename it CloseButton.

    Explorer window showing a new ImageButton inserted into SettingsMenu and renamed to CloseButton. Empty ImageButton positioned in upper-left corner of the settings menu.
  2. With CloseButton selected, set the following properties:

  3. Insert a UIAspectRatioConstraint into the button with default properties (1:1 width to height).

    Explorer window showing a UIAspectRatioConstraint inserted into the CloseButton button. Finalized close button in upper-right corner of settings menu.

Create the Control Modules

An extensible control module setup makes interactive UI management more streamlined than individual scripts placed within each object. ModuleScripts facilitate this extensible functionality by letting you reuse code between scripts on different sides of the client‑server boundary or the same side of the boundary.

Stateful Object Controller

The following stateful object controller module lets you attach behavior to UI objects such as SettingsButton and SettingsMenu, and easily toggle/tween between various states. To create the module:

  1. Insert a ModuleScript into the ReplicatedStorage container and rename it to StatefulObjectController.

    Explorer window showing the StatefulObjectController module inside the ReplicatedStorage container.
  2. Paste the following code inside the module:

    StatefulObjectController

    local TweenService = game:GetService("TweenService")
    local StatefulObjectController = {}
    StatefulObjectController.__index = StatefulObjectController
    export type StateName = string
    export type State = {
    transition: TweenInfo,
    properties: { [string]: any },
    }
    function StatefulObjectController.hydrate(props: {
    object: Instance,
    states: { [StateName]: State },
    initialStateName: StateName
    })
    local object, states, initialStateName = props.object, props.states, props.initialStateName
    local self = setmetatable({
    states = states,
    currentStateName = initialStateName,
    tweens = {},
    }, StatefulObjectController)
    -- Create tweens for reuse to avoid making new tweens every time state is changed
    for stateName, state in states do
    self.tweens[stateName] = TweenService:Create(object, state.transition, state.properties)
    end
    self:setState(self.currentStateName)
    return self
    end
    function StatefulObjectController:setState(stateName: StateName)
    local stateTween: Tween = self.tweens[stateName]
    if not stateTween then
    warn(string.format("Attempted to set %s to unknown state '%s'", self.object:GetFullName(), stateName))
    return
    end
    self.currentStateName = stateName
    -- Make sure other tweens aren't conflicting
    for _, tween in self.tweens do
    tween:Cancel()
    end
    stateTween:Play()
    end
    return StatefulObjectController
    LinesPurpose
    610Defines and exports the Luau types to improve autocomplete and linting when using this module.
    1233Function used to initialize and attach behavior to a stateful UI object, accepting a GuiObject as object, a table of states and their respective TweenInfo data, and an initial state name. Inside this function, lines 2628 construct Tweens for each state, and line 30 calls the module's setState() function to set the object to the initial state.
    3550Function used to set/change the state of a stateful object. Line 42 sets the object's currentStateName to the state being set, lines 4547 cancel any running tweens to prevent conflicts, and line 49 executes the state's tween(s).

Slider Controller

An additional module initializes and controls the two volume sliders. It also allows you to connect a callback function to each slider in order to detect player interaction with the slider and apply desired changes in the experience.

  1. Insert a ModuleScript into the ReplicatedStorage container and rename it to SliderController.

    Explorer window showing the SliderController module inside the ReplicatedStorage container.
  2. Paste the following code inside the module:

    SliderController

    local SliderController = {}
    SliderController.__index = SliderController
    export type Value = number
    export type OnChanged = (Value) -> ()
    function SliderController.hydrate(props: {
    object: Instance,
    onChanged: OnChanged,
    initialValue: Value?
    })
    local object, onChanged, initialValue = props.object, props.onChanged, props.initialValue
    local handle = object:FindFirstChild("Handle", true)
    if not handle then
    warn(string.format("Attempted to hydrate slider %s but couldn't find Handle", object:GetFullName()))
    end
    local innerFill = object:FindFirstChild("InnerFill", true)
    if not innerFill then
    warn(string.format("Attempted to hydrate slider %s but couldn't find InnerFill", object:GetFullName()))
    end
    local dragDetector = handle:FindFirstChildWhichIsA("UIDragDetector")
    if not dragDetector then
    warn(string.format("Attempted to hydrate slider %s but couldn't find UIDragDetector", object:GetFullName()))
    end
    local self = setmetatable({
    handle = handle,
    innerFill = innerFill,
    dragDetector = dragDetector,
    value = initialValue or 0.5,
    onChanged = onChanged,
    }, SliderController)
    -- Set initial value
    self:setValue(self.value)
    -- Connect detector to player manipulation
    self.dragConnection = dragDetector.DragContinue:Connect(function()
    self:setValue(handle.Position.X.Scale)
    end)
    return self
    end
    function SliderController:setValue(value: Value)
    local clampedValue = math.clamp(value, 0, 1)
    self.value = clampedValue
    -- Update the handle position and inner frame size to match
    self.handle.Position = UDim2.fromScale(clampedValue, 0.5)
    self.innerFill.Size = UDim2.fromScale(clampedValue, 1)
    -- Run the user's callback with the latest value
    local changeSuccess, changeResult = pcall(self.onChanged, clampedValue)
    if not changeSuccess then
    warn("Error in slider callback:", changeResult)
    end
    end
    return SliderController
    LinesPurpose
    746Function used to initialize and attach behavior to a slider element, accepting the slider's parent Frame as object, an onChanged callback function to process slider changes, and an initial value for the slider's handle position. Inside the function, lines 1427 confirm that the slider's handle, inner fill, and UIDragDetector elements exist. Line 38 calls the module's setValue() function and lines 4143 connect the drag detector's DragContinue event to the module's setValue() function (see next row for details).
    4861Function used to set the value of the slider. Inside the function, lines 5354 ensure the handle's position and inner fill size are synced, and line 57 passes the changed value to the callback function so that you can utilize the value as needed.

Create the Settings Script

With the settings button and settings menu finalized, you can hook everything together with a single script that utilizes the control modules.

  1. Insert a new LocalScript into HUDContainer and rename it to SettingsScript to describe its purpose. Note that this script should be at the same level (sibling) as SettingsMenu and SettingsButton, the top‑level UI objects it will manage.

    Explorer window showing the SettingsScript script inside the HUDContainer container.
  2. Paste the following code inside the script:

    SettingsScript

    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    local SoundService = game:GetService("SoundService")
    local SliderController = require(ReplicatedStorage.SliderController)
    local StatefulObjectController = require(ReplicatedStorage.StatefulObjectController)
    local HUDContainer = script.Parent
    -- Initialize settings button
    local settingsButton = StatefulObjectController.hydrate({
    object = HUDContainer:FindFirstChild("SettingsButton"),
    states = {
    menuOpen = {
    transition = TweenInfo.new(0.5, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out),
    properties = {
    Rotation = 45,
    },
    },
    menuClosed = {
    transition = TweenInfo.new(0.5, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out),
    properties = {
    Rotation = 0,
    },
    },
    },
    initialStateName = "menuClosed"
    })
    -- Initialize settings menu frame
    local settingsMenu = StatefulObjectController.hydrate({
    object = HUDContainer:FindFirstChild("SettingsMenu"),
    states = {
    menuOpen = {
    transition = TweenInfo.new(0.5, Enum.EasingStyle.Bounce, Enum.EasingDirection.Out),
    properties = {
    Position = UDim2.fromScale(0.5, 0.5),
    Visible = true,
    },
    },
    menuClosed = {
    transition = TweenInfo.new(0),
    properties = {
    Position = UDim2.fromScale(0.5, 0.4),
    Visible = false,
    },
    },
    },
    initialStateName = "menuClosed"
    })
    -- Initialize effects volume slider
    local effectsAudio = SoundService:FindFirstChild("Effects")
    local effectsVolumeSlider = SliderController.hydrate({
    object = HUDContainer:FindFirstChild("EffectsVolumeSlider", true),
    initialValue = effectsAudio and effectsAudio.Volume or 0.5,
    onChanged = function(value: SliderController.Value)
    if effectsAudio then
    effectsAudio.Volume = value
    end
    end,
    })
    -- Initialize background volume slider
    local backgroundAudio = SoundService:FindFirstChild("Background")
    local backgroundVolumeSlider = SliderController.hydrate({
    object = HUDContainer:FindFirstChild("BackgroundVolumeSlider", true),
    initialValue = backgroundAudio and backgroundAudio.Volume or 0.5,
    onChanged = function(value: SliderController.Value)
    if backgroundAudio then
    backgroundAudio.Volume = value
    end
    end,
    })
    -- Connect buttons to player interaction
    HUDContainer:FindFirstChild("SettingsButton").Activated:Connect(function()
    local targetState = if settingsButton.currentStateName == "menuClosed"
    then "menuOpen"
    else "menuClosed"
    settingsButton:setState(targetState)
    settingsMenu:setState(targetState)
    end)
    HUDContainer:FindFirstChild("CloseButton", true).Activated:Connect(function()
    settingsButton:setState("menuClosed")
    settingsMenu:setState("menuClosed")
    end)
  3. Reference the following sections to explore how the script utilizes the control modules.

    Lines 10-27 initialize and attach behavior to the settings button as follows:

    LinesPurpose
    1011A local reference settingsButton is declared and hydrated with the StatefulObjectController.hydrate() function, and the SettingsButton button within HUDContainer is referenced as the object to hydrate.
    1225A states table is passed with two unique states, menuOpen and menuClosed. Each state contains a transition to declare (through a TweenInfo) how the particular state will reach its target properties defined in the properties table. In this case, menuOpen declares a state change to 45° for the button's Rotation with an Exponential tween of ½ second, while menuClosed effectively restores the button to its default rotation of 0.
    26The button's initial state is set to menuClosed (this could be set to any state name within the states table).

    Further down in the script (lines 76-86), buttons are connected to player interaction to trigger UI state changes:

    LinesPurpose
    7682SettingsButton is connected to the Activated event of the GuiButton class to call an anonymous function. Inside the anonymous function, lines 7779 toggle a targetState variable between menuOpen and menuClosed. Line 80 then calls setState() to set the button's state to targetState and line 81 does the same to SettingsMenu so that it toggles open or closed with activation of SettingsButton.
    8386CloseButton is connected to the Activated event to call an anonymous function which sets the state of both SettingsButton and SettingsMenu to menuClosed.

    Lines 30-49 initialize and attach behavior to the settings menu as follows:

    LinesPurpose
    3031A local reference settingsMenu is declared and hydrated with the StatefulObjectController.hydrate() function and the SettingsMenu frame within HUDContainer is referenced as the object to hydrate.
    3246Similar to the settings button, a states table is passed with two unique states, menuOpen and menuClosed. Here, menuOpen declares a state change to UDim2.fromScale(0.5, 0.5) for the frame's Position with a Bounce tween of ½ second, as well as a Visible state of true. In contrast, menuClosed declares a positional state change to UDim2.fromScale(0.5, 0.4) and Visible to false, but the TweenInfo with 0 duration on line 41 effectively makes the state change occur instantly.
    48The frame's initial state is set to menuClosed.

    Further down in the script (lines 76-86), the settings menu is linked to player interaction:

    LinesPurpose
    81Sets the same targetState for SettingsMenu to sync with activation of SettingsButton.
    85Sets the state of SettingsMenu to menuClosed when CloseButton is activated.

    Lines 51-73 initialize and attach behavior to EffectsVolumeSlider and BackgroundVolumeSlider as follows:

    LinesPurpose
    52Sets a local reference to the Effects SoundGroup within SoundService. A SoundGroup lets you interrelate multiple Sounds and control the volume of every sound in the group through the group's Volume property.
    5354A local reference effectsVolumeSlider is declared and hydrated with the SliderController.hydrate() function and the EffectsVolumeSlider frame within HUDContainer ⟩ SettingsMenu is referenced as the object to hydrate. Note that the second parameter of true for FindFirstChild() tells the method to look recursively down the HUDContainer branch to locate EffectsVolumeSlider.
    55The slider's initial value is set to the current Volume of the Effects sound group, or 0.5 as a fallback.
    5660Establishes the callback function to apply slider value changes to the volume of the Effects sound group.
    6473Essentially the same as lines 52-61, except to hydrate the BackgroundVolumeSlider frame within HUDContainer ⟩ SettingsMenu and apply slider value changes to the volume of the Background sound group.

With SettingsScript in place, the experience now offers a fully operational interactive UI example that links a related set of UI objects to player interaction.