Choosing an Animation Approach in A-Frame: Built‑in, GSAP, or Custom Tick Loop
Decide whether to use A-Frame's <a-animation> component, GSAP integration, or a custom requestAnimationFrame loop based on animation complexity and control needs.
25 Apr 2026, 19:24 UTC

Decision and constraints
You need to animate properties of an <a-entity> in A‑Frame (version 1.4.x). The decision depends on three constraints:
- Complexity: simple one‑off tweens vs. sequenced or interactive motions.
- Control: need to pause, reverse, or dynamically change parameters from script.
- Bundle size: willingness to add external library weight.
If any of these constraints are strict, pick the option that best satisfies them while keeping the implementation maintainable.
Supported options
| Option | How it works | Pros | Cons | Approx. gzipped impact |
|---|---|---|---|---|
<a-animation> (built‑in) | Declarative component that reads attributes like attribute, from, to, dur, easing and drives the property automatically. | Zero extra library, automatic start/stop, easy to read in markup. | Limited to a single timeline, difficult to pause/reverse from script, no plugin ecosystem. | 0 KB |
| GSAP via custom component | Load GSAP from a CDN, create a component that builds a gsap.timeline() on init and kills it on remove. | Full timeline control, sequencing, plugins (e.g., ScrollTrigger), easy pause/resume. | Requires manual cleanup, adds library weight, version compatibility check needed. | ~30 KB (GSAP core) |
Custom requestAnimationFrame tick loop | Component registers a tick listener on the scene and updates a property using elapsed time and an easing function. | No extra weight, complete freedom to drive any property or data‑driven motion. | Boilerplate for timing/easing, risk of frame‑drop bugs if not cleaned up. | 0 KB |
Trade‑offs
For simple property tweens that run once per entity (e.g., a button that scales on hover), the built‑in <a-animation> gives the least code and best performance because the animation is driven by A‑Frame’s internal render loop without extra JavaScript.
When you need interactive control—starting, pausing, reversing, or chaining multiple animations—GSAP’s timeline API reduces the amount of state you must manage yourself. The trade‑off is the additional ~30 KB download and the responsibility to call timeline.kill() when the entity is removed to avoid memory leaks.
If your animation is highly dynamic, driven by external data (e.g., audio amplitude, physics simulation) or requires per‑frame custom logic, a custom tick loop offers the greatest flexibility. You must implement your own easing or use a small helper like AFRAME.utils.Tween, and ensure the tick listener is removed in the component’s remove handler.
Concrete implementation
Using the built‑in <a-animation> component
Place the following markup in a file named index.html and open it in a browser (no special permissions required).
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>A‑Frame built‑in animation</title>
<script src="https://aframe.io/releases/1.4.0/aframe.min.js"></script>
</head>
<body>
<a-scene>
<a-entity>
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9">
<a-animation attribute="position"
to="-1 2 -3"
dur="1000"
easing="ease-in-out"
direction="alternate"
repeat="indefinite">
</a-animation>
</a-box>
</a-entity>
</a-scene>
</body>
</html>
Verification: The box should move smoothly between y = 0.5 and y = 2 repeatedly. Open the browser’s developer tools and confirm that no JavaScript errors appear in the console.
Using GSAP via a custom component
First, add the GSAP core from a CDN. Then register a component that creates a timeline on init and destroys it on remove. The example below shows a pulsating scale animation that can be paused with a button.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>A‑Frame + GSAP</title>
<script src="https://aframe.io/releases/1.4.0/aframe.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
</head>
<body>
<a-scene>
<a-entity id="box" geometry="primitive: box" material="color: #FFC65D" position="0 1 -3">
</a-entity>
<a-entity>
<a-camera position="0 1.6 0"></a-camera>
</a-entity>
</a-scene>
<button id="pauseBtn" style="position: absolute; top: 10px; left: 10px;">Pause</button>
<script>
AFRAME.registerComponent('gsap-pulse', {
init: function () {
this.timeline = gsap.timeline({ repeat: -1, yoyo: true })
.to(this.el.object3D.scale, { x: 1.5, y: 1.5, z: 1.5, duration: 0.8, ease: 'power1.inOut' });
},
remove: function () {
this.timeline.kill();
}
});
// Attach component
document.querySelector('#box').setAttribute('gsap-pulse', '');
// Pause/resume button
let paused = false;
document.getElementById('pauseBtn').addEventListener('click', () => {
const tl = document.querySelector('#box').components['gsap-pulse'].timeline;
if (paused) { tl.play(); } else { tl.pause(); }
paused = !paused;
document.getElementById('pauseBtn').textContent = paused ? 'Resume' : 'Pause';
});
</script>
</body>
</html>
Verification: The box should continuously scale up and down. Clicking the “Pause” button should freeze the animation; clicking again resumes it. Removing the entity (e.g., via document.querySelector('#box').remove()) should stop the timeline and prevent further GSAP callbacks—check the console for no warnings.
Using a custom tick loop
This approach drives rotation based on elapsed time, useful for data‑driven motion such as reacting to audio frequency. The component registers a tick listener and cleans it up on remove.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>A‑Frame custom tick</title>
<script src="https://aframe.io/releases/1.4.0/aframe.min.js"></script>
</head>
<body>
<a-scene>
<a-entity id="spinner" geometry="primitive: torus" material="color: #7BC8A4" position="0 1 -3">
</a-entity>
<a-entity>
<a-camera position="0 1.6 0"></a-camera>
</a-entity>
</a-scene>
<script>
AFRAME.registerComponent('tick-spin', {
init: function () {
this.speed = Math.PI / 2; // radians per second
},
tick: function (time, delta) {
// delta is milliseconds since last tick; convert to seconds
const seconds = delta / 1000;
this.el.object3D.rotation.y += this.speed * seconds;
},
remove: function () {
// No explicit listener to remove; the tick handler is automatically garbage‑collected
// when the component is deleted. If you used sceneEl.addEventListener('tick', ...),
// you would call removeEventListener here.
}
});
document.querySelector('#spinner').setAttribute('tick-spin', '');
</script>
</body>
</html>
Verification: The torus should rotate smoothly at a constant speed. Open the console and confirm that no errors appear. Removing the entity stops the rotation because the component’s tick handler is no longer invoked.
Limitations and practical checks
- Built‑in: Cannot easily change
from/tovalues at runtime without removing and re‑adding the component. - GSAP: Ensure you load a version compatible with A‑Frame 1.4.x (GSAP 3.x works). Forgetting to call
timeline.kill()on entity removal leaves stray animations that continue to consume CPU. - Custom tick: You must provide your own easing functions if needed; a mistake in the delta calculation can cause jitter or frame‑drop.
To check that only one system is animating a given property, temporarily set the property via the inspector (e.g., document.querySelector('#box').object3D.position.set(0,0,0)) and observe whether the animation overrides your manual change. If it does, you know that system is still active.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.