Orbit Stagger
GSAPA pulsing purple core with three dots orbiting at staggered speeds, all returning to rest together.
- Tier
- GSAP
- Dependencies
- gsap
- Duration
- 6 s, loops
- Categories
- Icons and micro-interactions
- Use cases
- Loading and waiting, AI and processing
- Industries
- SaaS and software, Cross-industry
- Style
- Duotone · none stroke
- Set
- nebula
- Tags
- orbitstaggerloopbrandcircle
- Themeable
- No. Fixed palette.
- Version
- 1.0.0, added 2026-09-08
- Licence
- To be confirmed. See the repository LICENSE.
Code
animation.svg
<svg xmlns="http://www.w3.org/2000/svg" aria-labelledby="orbit-stagger-title" class="pulsar pulsar-orbit-stagger" data-pulsar="orbit-stagger" role="img" viewBox="0 0 100 100"><title id="orbit-stagger-title">Orbit stagger</title><defs><radialGradient id="orbit-stagger-core-grad" cx="50%" cy="50%" r="50%"><stop offset="0" stop-color="#a27ffe"></stop><stop offset=".55" stop-color="#7a46ff"></stop><stop offset="1" stop-color="#5430af"></stop></radialGradient></defs><g id="orbit-stagger-orbit-1"><circle id="orbit-stagger-ring-1" cx="50" cy="50" r="16" fill="none" stroke="#5430af" stroke-width=".75" opacity=".55"></circle><circle id="orbit-stagger-dot-1" cx="50" cy="34" r="2.2" fill="#a27ffe"></circle></g><g id="orbit-stagger-orbit-2"><circle id="orbit-stagger-ring-2" cx="50" cy="50" r="26" fill="none" stroke="#5430af" stroke-width=".75" opacity=".45"></circle><circle id="orbit-stagger-dot-2" cx="76" cy="50" r="2.6" fill="#7a46ff"></circle></g><g id="orbit-stagger-orbit-3"><circle id="orbit-stagger-ring-3" cx="50" cy="50" r="36" fill="none" stroke="#5430af" stroke-width=".75" opacity=".35"></circle><circle id="orbit-stagger-dot-3" cx="24.54" cy="75.46" r="2" fill="#5430af"></circle></g><circle id="orbit-stagger-halo" cx="50" cy="50" r="10" fill="#a27ffe" opacity=".22"></circle><circle id="orbit-stagger-core" cx="50" cy="50" r="6" fill="url(#orbit-stagger-core-grad)"></circle></svg>
animation.js
;(function () {
var el = document.querySelector('[data-pulsar="orbit-stagger"]')
if (!el || typeof gsap === 'undefined') return
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
var orbits = [
el.querySelector('#orbit-stagger-orbit-1'),
el.querySelector('#orbit-stagger-orbit-2'),
el.querySelector('#orbit-stagger-orbit-3'),
]
var core = el.querySelector('#orbit-stagger-core')
var halo = el.querySelector('#orbit-stagger-halo')
// One cycle is 6 s. Each orbit group holds its ring and its dot, so
// '50% 50%' of the group is the ring centre. Inner ring turns three
// times, middle twice, outer once, so all return to rest at exactly 6 s.
var tl = gsap.timeline({ repeat: -1, paused: reduced })
var turns = [3, 2, 1]
for (var i = 0; i < orbits.length; i++) {
if (!orbits[i]) continue
tl.to(
orbits[i],
{ rotation: 360 * turns[i], transformOrigin: '50% 50%', ease: 'none', duration: 6 },
0,
)
}
if (core) {
tl.to(
core,
{
scale: 1.12,
transformOrigin: '50% 50%',
ease: 'sine.inOut',
duration: 3,
yoyo: true,
repeat: 1,
},
0,
)
}
if (halo) {
tl.to(
halo,
{
scale: 1.3,
opacity: 0.1,
transformOrigin: '50% 50%',
ease: 'sine.inOut',
duration: 3,
yoyo: true,
repeat: 1,
},
0,
)
}
el.pulsar = {
play: function () {
if (!reduced) tl.play()
},
pause: function () {
tl.pause()
},
seek: function (ms) {
if (!reduced) tl.seek(ms / 1000)
},
destroy: function () {
tl.kill()
},
}
})()
GSAP tier: this animation expects a global gsap. Load GSAP before the script.
