Roblox GUI Animations: From Sprite Sheet to ImageLabel
Create animated GUIs in Roblox using sprite sheets: ImageRectOffset, ImageRectSize, and scripting frame-by-frame.
In Roblox, GUI animations are often done by swapping the image displayed in an ImageLabel or by changing the ImageRectOffset and ImageRectSize of a single texture so it shows one "frame" of a sprite sheet. The second approach uses one image asset and one request, which is good for performance. This guide covers creating the sheet, uploading it, scripting the animation loop, and handling multiple animations or resolutions.
Creating the Sprite Sheet
Use a Video to Sprite Sheet or an atlas tool to produce one PNG with all frames in a row or grid, and export metadata (JSON) that lists each frame's x, y, width, and height. Roblox doesn't read that JSON natively; you'll use it in your script to set ImageRectOffset and ImageRectSize. Upload the PNG to Roblox as an image asset and get its asset ID or use it in an ImageLabel.
Layout matters: row layout (frames in one row) or grid layout both work. The JSON gives you the pixel position and size of each frame so you can set the rect correctly. Ensure the sheet fits within Roblox's image size limits (check the current docs for max dimensions). If you have multiple animations (e.g. idle, walk, jump), you can pack them all in one sheet and store separate start/end frame indices per animation in your script.
What you need from the export
Keep the JSON or convert it to a Lua table: each frame needs offset (x, y) and size (width, height) in pixels. You'll index into this table by frame number when updating the ImageLabel. The PNG should be uploaded to Roblox (Create → Decal or use the asset flow); reference it via the asset ID or rbxassetid in the ImageLabel's Image property.
Scripting the Animation
Store the frame list (from your JSON) in a table. Each entry has offset (x, y) and size (width, height). In a loop or a Heartbeat/RenderStepped connection, advance a frame index based on elapsed time and your desired FPS. Set ImageLabel.ImageRectOffset = Vector2.new(offsetX, offsetY) and ImageLabel.ImageRectSize = Vector2.new(width, height). The ImageLabel's Image should be the sprite sheet texture. That way the label shows only the current frame. Loop the frame index when it reaches the end for a looping animation.
Pseudocode: maintain a currentTime that increments by deltaTime each frame. Compute frameIndex = floor(currentTime * fps) % frameCount for a looping animation, then look up the rect for that index and assign ImageRectOffset and ImageRectSize. For one-shot animations, clamp the index and stop or fire an event when the animation completes. Match the FPS to the FPS you used when exporting the sheet so that timing looks correct.
Example structure
Create a module or script that holds the frame table (array of {offset = Vector2, size = Vector2}) and a function that takes elapsed time, FPS, and whether to loop, and returns the current frame index. The GUI script connects to Heartbeat or RenderStepped, updates elapsed time, gets the frame index, and sets the ImageLabel's rect from the table. Separate the frame data from the update logic so you can reuse it for multiple animations or labels.
Considerations
Roblox has image size limits; keep the sprite sheet under that. Use a single sheet for one character or one effect so you don't hit the limit with many small images. If you have many animations, you can use multiple sheets or pack several animations into one sheet and track offset per animation in your script.
Using one texture and rect switching is more efficient than using many separate ImageLabels or swapping the Image property between multiple asset IDs: one HTTP request, one texture in memory, and the GPU only samples a sub-rect. For mobile and lower-end devices, this can improve load time and frame rate. If you need to support multiple resolutions, consider exporting a 1x and 2x sheet and picking the appropriate asset based on device or scale factor.
Summary
Export a sprite sheet and JSON, upload the PNG to Roblox, and drive ImageRectOffset and ImageRectSize from a frame table and elapsed time. One texture per animation (or per character) keeps things efficient and avoids hitting asset limits.