Hey! I’ve been working on Xocoatl, a timeline-based UI animator for Studio, and I wanted to share a little preview.
It’s not out yet, but it might be soon. When it is, I’ll update this post.
I started making it because UI animation felt like something that should be way more visual and fun to work with. I wanted to be able to scrub through an animation, drop keyframes in, tweak things live, and actually see what I was doing, instead of feeling like I was guessing my way through it.
So that’s the idea behind this, no code in the editor, just a timeline, keyframes, playback, and a small runtime to make it work in-game.
What it does
You pick a UI element, open the timeline, and start animating properties like:
- Position
- Size
- Color
- Transparency
Scrub to a point in time, change a value, move somewhere else, change it again, and your keyframes are there. Hit play, and you can immediately see how it feels.
A few things that are already in there:
- easing per keyframe
- multi-select
- undo / redo
- live preview while editing
Once you’re happy with an animation, you save it and play it in-game with the runtime.
I tried to keep the whole editor side as visual and low-friction as possible.
Playing it in-game
You install the runtime once from the plugin, then from any LocalScript you can do stuff like:
local Anim = require(
game.Players.LocalPlayer
.PlayerScripts.Xocoatl.UIAnimController
)
-- Play once
Anim.play("MyAnimation")
-- Loop
Anim.play("MyAnimation", { loop = true })
-- When finished, do something
Anim.play("MyAnimation", {
onComplete = function()
Anim.play("IdleFloat", { loop = true })
end
})
-- Control by handle
local handle = Anim.play("SlideIn")
handle.pause()
handle.resume()
handle.scrub(0.5) -- seek to 0.5s
handle.stop()
Runtime features that are really handy
-
One clip, open and close — Design the “open” animation once; play it with
reverse = truefor close. No duplicate clips. -
Staggered list reveals —
Anim.playBatch(cards, "FadeIn", { stagger = 0.06 })runs the same clip on many elements with a delay between each. One clip, cascade effect. -
Cleanup before hide/destroy —
Anim.clearInstance(frame)stops every animation on that element. Use it before settingVisible = falseor destroying UI so nothing keeps running. -
Delay and run —
Anim.play("ShowcaseAni", { delay = 3 })starts after 3 seconds. The runtime waits for your UI to exist, so you can call it as soon as the player joins. -
Reset on finish —
Anim.play("ClickPop", { reset = true })restores the element to how it was before the animation. Perfect for one-shot effects (pops, bounces) with zero cleanup code. -
Chain clips —
Anim.sequence({ { name = "SlideIn" }, { name = "Pulse", opts = { loop = 3 } }, { name = "SlideOut" } })runs them one after another. No nestedonCompletecallbacks. -
Control by name —
Anim.play("LoadingPulse", { loop = true })then laterAnim.stop("LoadingPulse"). No need to store the handle if you just want to stop by name. -
Event markers — Put named events on the timeline in the editor; at runtime
onEvent = function(name)fires when the playhead crosses them (e.g. play a sound, show text).
So yeah — this is a preview and the plugin isn’t out yet. I’m just one person on this and it’s still evolving. I can’t really get your opinion on some things until you have it — once it’s released I’ll update this post for sure and then I’d love to hear what you think (more properties, export/import, or just “I wish it did X”). For now, no pressure, just sharing what I’ve been building.
Thanks for taking a look — happy to answer questions in the thread.
Chocobasta out <3


