Tutorial · Menus and sound

How to animate UI in Roblox Studio with TweenService

The difference between a menu that feels cheap and one that feels finished is often a quarter of a second of movement: a button that grows when you hover, dips when you press, a shop that pops open instead of appearing. This guide puts those effects in one small module and applies them to every button in your game with a single LocalScript.

Updated 23 October 20265 min readBeginnerLuau

In short

Use TweenService:Create(object, TweenInfo.new(time, style, direction), { Property = value }):Play(). For buttons, tween a UIScale inside the button: 1.06 on MouseEnter, 0.94 on MouseButton1Down, back to 1 on leave. For menus, set the UIScale to 0.6, make the frame visible and tween to 1 with the Back easing style for a small overshoot.

  • Tween a UIScale, not Size: a UIScale grows the frame and everything inside it, and leaves your layout alone.
  • Keep UI tweens short, 0.1 to 0.35 seconds. Longer feels slow.
  • Hide a closing panel only when its tween has completed, so it does not vanish mid-animation.

Get the script

UI animation module for buttons and menus

A ModuleScript with hover and press effects for buttons, pop open and close for panels, and a slide-in, all built on TweenService and UIScale.

ModuleScript in ReplicatedStorage named UIAnimate
-- ModuleScript in ReplicatedStorage named UIAnimate.
-- Small tween helpers for menus: hover and press on buttons, pop a panel in and out,
-- slide a panel from off screen. Use it from any LocalScript.
local TweenService = game:GetService("TweenService")

local UIAnimate = {}

local QUICK = TweenInfo.new(0.12, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local POP = TweenInfo.new(0.25, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
local CLOSE = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.In)
local SLIDE = TweenInfo.new(0.35, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)

-- A UIScale grows or shrinks a frame and everything inside it, without changing its Size
local function getScale(object: GuiObject): UIScale
	local scale = object:FindFirstChildOfClass("UIScale")
	if not scale then
		local newScale = Instance.new("UIScale")
		newScale.Parent = object
		return newScale
	end
	return scale
end

-- Grow a little on hover, shrink on press, back on release
function UIAnimate.button(button: GuiButton)
	local scale = getScale(button)
	local hovering = false
	local function to(value: number)
		TweenService:Create(scale, QUICK, { Scale = value }):Play()
	end
	button.MouseEnter:Connect(function()
		hovering = true
		to(1.06)
	end)
	button.MouseLeave:Connect(function()
		hovering = false
		to(1)
	end)
	button.MouseButton1Down:Connect(function()
		to(0.94)
	end)
	button.MouseButton1Up:Connect(function()
		to(if hovering then 1.06 else 1)
	end)
end

-- Open: show the panel and pop it from small to full size with a slight overshoot
function UIAnimate.open(panel: GuiObject)
	local scale = getScale(panel)
	scale.Scale = 0.6
	panel.Visible = true
	TweenService:Create(scale, POP, { Scale = 1 }):Play()
end

-- Close: shrink, then hide
function UIAnimate.close(panel: GuiObject)
	local scale = getScale(panel)
	local tween = TweenService:Create(scale, CLOSE, { Scale = 0.6 })
	tween.Completed:Connect(function(state: Enum.PlaybackState)
		if state == Enum.PlaybackState.Completed then
			panel.Visible = false
		end
	end)
	tween:Play()
end

-- Slide in from the left edge of the screen to where the panel was placed in Studio
function UIAnimate.slideIn(panel: GuiObject)
	local target = panel:GetAttribute("HomePosition")
	local home = if typeof(target) == "UDim2" then target else panel.Position
	panel:SetAttribute("HomePosition", home)
	panel.Position = UDim2.new(-1, 0, home.Y.Scale, home.Y.Offset)
	panel.Visible = true
	TweenService:Create(panel, SLIDE, { Position = home }):Play()
end

return UIAnimate

RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts

Where to put it

Explorer: the module and the script that uses it
ReplicatedStorage
  UIAnimate ModuleScript new
StarterPlayer
  StarterPlayerScripts
    AnimateMenus LocalScript new

Settings you can change

POPIn the script
The opening animation: a quarter second with a small overshoot. QUICK, CLOSE and SLIDE beside it set the others. Default: TweenInfo.new(0.25, Enum.EasingStyle.Back, Enum.EasingDirection.Out).

Use it with the AnimateMenus LocalScript from Step 2, which gives every button the effect and opens panels from buttons with an Opens attribute.

Type-checked with luau-lsp strict against Roblox's API definitions on 23 October 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.

Prompt for RoCode

Animate my game's UI: a UIAnimate ModuleScript in ReplicatedStorage with TweenService helpers for button hover and press using UIScale, pop open and close for panels, and slide in, plus a LocalScript in StarterPlayerScripts that applies the button effect to every GuiButton and lets buttons with an Opens attribute open and close the frame with that name.

On this page
  1. Get the script
  2. How it works
  3. 1. Module
  4. 2. Apply
  5. Common errors
  6. With RoCode
  7. Questions
  8. Sources

How UI tweens work

A tween moves a property from its current value to a target over time. TweenInfo sets how long it takes and how it moves: the easing style is the shape of the motion and the direction says whether it eases at the start, the end or both.

Easing styles for UI
StyleFeels likeUse for
Quad, OutFast start, gentle stopHover and press
Back, OutGoes slightly past, then settlesOpening a panel
Quint, OutVery fast, then a long glideSliding in from the side
LinearConstant speedProgress bars, spinning icons

Step 1: The animation module

Insert a ModuleScript in ReplicatedStorage named UIAnimate:

ModuleScript in ReplicatedStorage named UIAnimate
-- ModuleScript in ReplicatedStorage named UIAnimate.
-- Small tween helpers for menus: hover and press on buttons, pop a panel in and out,
-- slide a panel from off screen. Use it from any LocalScript.
local TweenService = game:GetService("TweenService")

local UIAnimate = {}

local QUICK = TweenInfo.new(0.12, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local POP = TweenInfo.new(0.25, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
local CLOSE = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.In)
local SLIDE = TweenInfo.new(0.35, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)

-- A UIScale grows or shrinks a frame and everything inside it, without changing its Size
local function getScale(object: GuiObject): UIScale
	local scale = object:FindFirstChildOfClass("UIScale")
	if not scale then
		local newScale = Instance.new("UIScale")
		newScale.Parent = object
		return newScale
	end
	return scale
end

-- Grow a little on hover, shrink on press, back on release
function UIAnimate.button(button: GuiButton)
	local scale = getScale(button)
	local hovering = false
	local function to(value: number)
		TweenService:Create(scale, QUICK, { Scale = value }):Play()
	end
	button.MouseEnter:Connect(function()
		hovering = true
		to(1.06)
	end)
	button.MouseLeave:Connect(function()
		hovering = false
		to(1)
	end)
	button.MouseButton1Down:Connect(function()
		to(0.94)
	end)
	button.MouseButton1Up:Connect(function()
		to(if hovering then 1.06 else 1)
	end)
end

-- Open: show the panel and pop it from small to full size with a slight overshoot
function UIAnimate.open(panel: GuiObject)
	local scale = getScale(panel)
	scale.Scale = 0.6
	panel.Visible = true
	TweenService:Create(scale, POP, { Scale = 1 }):Play()
end

-- Close: shrink, then hide
function UIAnimate.close(panel: GuiObject)
	local scale = getScale(panel)
	local tween = TweenService:Create(scale, CLOSE, { Scale = 0.6 })
	tween.Completed:Connect(function(state: Enum.PlaybackState)
		if state == Enum.PlaybackState.Completed then
			panel.Visible = false
		end
	end)
	tween:Play()
end

-- Slide in from the left edge of the screen to where the panel was placed in Studio
function UIAnimate.slideIn(panel: GuiObject)
	local target = panel:GetAttribute("HomePosition")
	local home = if typeof(target) == "UDim2" then target else panel.Position
	panel:SetAttribute("HomePosition", home)
	panel.Position = UDim2.new(-1, 0, home.Y.Scale, home.Y.Offset)
	panel.Visible = true
	TweenService:Create(panel, SLIDE, { Position = home }):Play()
end

return UIAnimate

getScale finds or adds a UIScale. button connects hover and press. open and close pop a panel; close hides it only when the tween reached the end (PlaybackState.Completed), so opening again mid-close does not get hidden by the old tween. slideIn remembers where you placed the panel in a HomePosition attribute, moves it off the left edge and tweens it back.

Step 2: Apply it to every button

Insert a LocalScript in StarterPlayer > StarterPlayerScripts named AnimateMenus. It runs once per player, and gives every button in their ScreenGuis the hover and press effect, including buttons added later, such as GUIs that are copied in again after a respawn:

LocalScript in StarterPlayerScripts named AnimateMenus
-- LocalScript in StarterPlayerScripts named AnimateMenus.
-- Gives every button in your ScreenGuis the hover and press animation, and makes
-- buttons with an "Opens" attribute open and close the frame with that name.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

type UIAnimate = {
	button: (GuiButton) -> (),
	open: (GuiObject) -> (),
	close: (GuiObject) -> (),
	slideIn: (GuiObject) -> (),
}
local UIAnimate = require(ReplicatedStorage:WaitForChild("UIAnimate")) :: UIAnimate

local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
local done: { [Instance]: boolean } = {}

local function setUp(object: Instance)
	if done[object] or not object:IsA("GuiButton") then
		return
	end
	done[object] = true
	UIAnimate.button(object)
	-- Opens = "ShopFrame": this button opens or closes the frame named ShopFrame in the same ScreenGui
	local opens = object:GetAttribute("Opens")
	local screen = object:FindFirstAncestorOfClass("ScreenGui")
	if typeof(opens) == "string" and screen then
		object.Activated:Connect(function()
			local panel = screen:FindFirstChild(opens, true)
			if panel and panel:IsA("GuiObject") then
				if panel.Visible then
					UIAnimate.close(panel)
				else
					UIAnimate.open(panel)
				end
			end
		end)
	end
end

for _, object in playerGui:GetDescendants() do
	setUp(object)
end
playerGui.DescendantAdded:Connect(setUp)
  1. For a button that opens a menu, add a String attribute named Opens to the button, with the name of the frame, such as ShopFrame.
  2. Put that frame in the same ScreenGui and untick its Visible.
  3. Press Play and click the button: the shop pops open. Click again: it closes.

The script requires the module with a type annotation, so strict type checking knows its functions. From any other LocalScript, call UIAnimate.slideIn(frame) or UIAnimate.open(frame) the same way.

Test it

  1. Press Play and hover over any button: it grows slightly. Press and hold: it shrinks. Release: it grows back.
  2. Click a button with an Opens attribute: its frame pops open with a small bounce, and closes on the next click.
  3. Test in the Device Simulator with a phone: taps press the button; there is no hover on touch screens.

Common errors and fixes

If nothing animates, check the Output window first.

Buttons do not animate

AnimateMenus must be a LocalScript in StarterPlayerScripts, and UIAnimate a ModuleScript in ReplicatedStorage with that exact name.

The panel jumps instead of growing

Something else sets its Size every frame, or it has a UISizeConstraint that fights the UIScale. Animate the UIScale only.

The Opens button does nothing

The attribute must be a String with the exact name of a GuiObject in the same ScreenGui.

Text looks blurry while it grows

That is normal mid-tween. Keep scales between about 0.9 and 1.1 for buttons.

Build it with RoCode

If you would rather not set this up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the scripts in your Explorer.

You type

Make every button in my game grow on hover and shrink on click, and make the shop and settings panels pop open and closed.

RoCode does
  • Can insert ready-made, animated screens from its premium UI kits in one step: HUD, pet inventory, store, daily rewards, item shop, egg hatching, rebirth, settings, daily quests and a loading screen, in five themes (candy, neon, gold, blocky, pastel), with working server scripts
  • Or adds the animation module from this guide to the GUIs you already have
  • Is told to compile-check each script with Check Script, and sets undo points around each batch

After a build, RoCode can run its test robot: it plays your game in Studio, clicks buttons and reports PASS or FAIL. It checks only what it chose to test, so judge the feel yourself. How RoCode connects to Studio.

Questions

How do I tween UI in Roblox?

TweenService:Create(guiObject, TweenInfo.new(0.25), { Position = UDim2.fromScale(0.5, 0.5) }):Play() moves a frame. Any number, UDim2 or Color3 property can be tweened.

How do I make a button grow when hovered?

Put a UIScale in the button and tween its Scale to about 1.06 on MouseEnter and back to 1 on MouseLeave.

Should I tween Size or use UIScale?

UIScale. It scales the frame and all its children together and does not change the Size you set in Studio.

Why does my closing animation get cut off?

The frame was hidden before the tween ended. Hide it in the tween's Completed event.

Sources and further reading

How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 23 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.