How to Make a Draggable GUI in Roblox — Complete Roblox Guide
Last updated: 2026-06-20 · 703 words · 4 min read
A draggable GUI lets the player reposition a panel — chat window, minimap, settings menu, dev console — anywhere on screen. Roblox does not ship a built-in Draggable property anymore (it was deprecated in 2023), so you write the behavior yourself in 30 lines of Luau. This guide gives you a copy-paste pattern that works on mouse, touch, and gamepad, plus the screen-clamping logic that prevents players from dragging the panel off-screen and stranding it.
Why Frame.Draggable was deprecated
Roblox removed Frame.Draggable because the property only worked with mouse input — touch and gamepad were silently broken. Manually implementing drag with UserInputService is more code, but the result handles every input type, supports modifiers (drag only when Ctrl is held, for example), and lets you clamp the panel to the screen. The deprecation is a feature: it forced everyone onto a more robust pattern.
The draggable pattern
Listen to InputBegan on the frame to start dragging when MouseButton1 or Touch begins. 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. The trick is using UserInputService rather than the Frame's own InputChanged — that way you keep dragging even if the cursor leaves the frame's bounds during the drag.
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 will accidentally drag the panel off the edge and lose it. Read AbsoluteSize on the screen and the frame, calculate the maximum X/Y the frame can occupy, and clamp the new Position to that range. Add the clamp inside InputChanged before assigning Position.
-- Inside InputChanged, after computing delta:
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
Sometimes the entire panel should not be draggable — only a title bar at the top. Move the InputBegan listener from the frame to a child Frame named DragHandle. The math is identical; only the input source changes. This is the standard pattern for dev consoles, debug overlays, and chat windows.
Persisting the position with DataStore
Players expect their layout choices to survive a server hop. After every drag ends, write the final UDim2 components to a DataStore keyed by UserId. On player join, read the saved values and assign them before the GUI becomes visible. Write to DataStore at most once every five seconds (DataStore quota is generous but not infinite) — debounce with a delayed task or write on player leave instead.
Touch and gamepad gotchas
Touch input on Roblox fires InputBegan for both UserInputType.Touch and UserInputType.MouseButton1 (Roblox emulates mouse for compatibility). If you handle both you will start two simultaneous drags — solve it by only listening for one, or by guarding with a `dragging` flag. Gamepad does not have a native pointer; for gamepad-friendly UIs, support "select panel + d-pad to nudge position" instead of drag-and-drop.
Z-index, sibling overlap, and grabbing the right frame
When the player drags a panel over another panel, Roblox decides which one receives input by ZIndex. The higher ZIndex wins. If your draggable Frame keeps losing input to a sibling, raise its ZIndex (or its parent's) before the drag begins, and lower it back on InputEnded. Be aware that ZIndex only matters within the same ScreenGui — a Frame in ScreenGui A always sits above ScreenGui B with a lower DisplayOrder, regardless of ZIndex. The fix for "my draggable panel sometimes catches input on the HUD behind it" is usually a DisplayOrder bump on the parent ScreenGui.
Reset-to-default and out-of-bounds recovery
Even with screen clamping, an updated viewport (player rotates phone, joins on a different device) can leave a saved position outside the new screen. On player join, after restoring the saved position from DataStore, validate it against the current ViewportSize and snap it back inside if needed. Also expose a "Reset position" button somewhere — players appreciate the escape hatch when they accidentally drag the panel into a corner. Reset to the original UDim2 you set in the editor; do not just hard-code 0,0,0,0 because that puts the panel in the top-left corner where it overlaps the Roblox top bar.
Step-by-step summary
- 1
Add the input listeners
Connect InputBegan, InputChanged via UserInputService, and InputEnded to track drag state across mouse and touch.
- 2
Apply the delta to Position
Add the input delta to the captured startPos offset components and assign back to frame.Position.
- 3
Clamp to screen bounds
Use math.clamp on the new X and Y so the frame stays fully inside ViewportSize − AbsoluteSize.
- 4
Move listeners to a drag handle (optional)
Restrict drag to a title bar by listening on a child Frame instead of the whole panel.
- 5
Persist to DataStore (optional)
Save the final position keyed by UserId on InputEnded; restore on player join before the GUI is visible.
FAQ
Why does the panel jump on the first drag?
You probably read frame.Position after the player started dragging. Capture startPos inside InputBegan (before the first delta) so the math has a stable baseline.
Does this work on mobile?
Yes — UserInputType.Touch is handled identically to MouseButton1. Test on a real device or Roblox Studio's mobile emulator to confirm.
Can I limit dragging to one axis?
Yes. Drop the X or Y term from the Position update — for example, keep startPos.X.Offset constant to lock horizontal dragging.