How to Make a Draggable GUI in Roblox — Complete Roblox Guide
Last updated: 2026-06-20 · 253 words · 2 min read
A draggable GUI lets the player reposition a panel — chat window, minimap, settings menu. Roblox deprecated Frame.Draggable in 2023, so you write the behavior in ~30 lines of Luau. This guide gives a copy-paste pattern that works on mouse, touch, and gamepad, plus screen-clamping logic.
Why Frame.Draggable was deprecated
Roblox removed Frame.Draggable because it only worked with mouse input — touch and gamepad were silently broken. Manually implementing drag with UserInputService handles every input type and lets you clamp the panel to the screen.
The draggable pattern
Listen to InputBegan on the frame to start dragging. Capture the start position. Listen to InputChanged on UserInputService for every movement. Listen to InputEnded to stop. Update the frame's Position by adding the input delta to the captured start position.
local UserInputService = game:GetService("UserInputService")
local frame = script.Parent
local dragging = false
local dragStart, startPos
frame.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch then
dragging = true
dragStart = input.Position
startPos = frame.Position
input.Changed:Connect(function()
if input.UserInputState == Enum.UserInputState.End then
dragging = false
end
end)
end
end)
UserInputService.InputChanged:Connect(function(input)
if not dragging then return end
if input.UserInputType == Enum.UserInputType.MouseMovement
or input.UserInputType == Enum.UserInputType.Touch then
local delta = input.Position - dragStart
frame.Position = UDim2.new(
startPos.X.Scale,
startPos.X.Offset + delta.X,
startPos.Y.Scale,
startPos.Y.Offset + delta.Y
)
end
end)Clamp to screen bounds
Without clamping, players drag the panel off the edge and lose it. Read AbsoluteSize on the screen and frame, calculate max X/Y, and clamp the new Position to that range.
local screenSize = workspace.CurrentCamera.ViewportSize
local frameSize = frame.AbsoluteSize
local newX = math.clamp(startPos.X.Offset + delta.X, 0, screenSize.X - frameSize.X)
local newY = math.clamp(startPos.Y.Offset + delta.Y, 0, screenSize.Y - frameSize.Y)
frame.Position = UDim2.new(0, newX, 0, newY)Adding a drag handle
Move the InputBegan listener from the frame to a child Frame named DragHandle. The math is identical; only the input source changes.
Persisting the position with DataStore
Players expect layout choices to survive a server hop. Write final UDim2 components to a DataStore keyed by UserId, and restore on player join.
Touch and gamepad gotchas
Touch input fires InputBegan for both Touch and MouseButton1. Guard with a dragging flag to avoid two simultaneous drags. Gamepad does not have a pointer; support select + d-pad to nudge instead.
Step-by-step summary
- 1
Add the input listeners
Connect InputBegan, InputChanged, and InputEnded to track drag state.
- 2
Apply the delta to Position
Add the input delta to startPos offset components.
- 3
Clamp to screen bounds
Use math.clamp so the frame stays inside ViewportSize − AbsoluteSize.
- 4
Move listeners to a drag handle
Restrict drag to a title bar if needed.
- 5
Persist to DataStore
Save final position keyed by UserId on InputEnded.
FAQ
Why does the panel jump on the first drag?
You read frame.Position after dragging started. Capture startPos inside InputBegan.
Does this work on mobile?
Yes — UserInputType.Touch is handled like MouseButton1.
Can I limit dragging to one axis?
Yes — keep startPos.X.Offset or Y.Offset constant.