4 min read

Feb 2022

Animating a Gradient Mesh with GSAP Motion Paths

I've always been fascinated by the balance of static design and motion design.
Gradient meshes are traditionally seen as background art — static, decorative, and flat. But I wanted to explore: what happens when a gradient mesh becomes dynamic, interactive, and alive?

This project was about transforming a simple mesh into a stage for motion. Instead of being just a backdrop, it became a system where my profile image and tech stack icons orbit endlessly, almost like planets circling in a digital solar system. It served as the hero section of my 2022 personal website, setting the tone for everything that followed.

Why Gradient Mesh?

Gradient meshes are a popular tool in design because they create soft, fluid transitions between colors. They look organic, almost like light scattering or atmospheric blending.

In my case, I already had a gradient mesh illustration designed in Figma and exported as an SVG. It looked beautiful, but static. So the question was: How do I make this gradient mesh feel alive without overwhelming the design?

The answer was motion, subtle, continuous, and balanced.

Motion as Orbit

Rather than distorting the mesh itself, I decided to place objects on top of it and animate those instead.

The objects:

  • My profile image
  • Logos of technologies I use daily: AWS, React, Next.js, GitHub, and TypeScript

Instead of moving them in straight lines, I wanted them to orbit around the center, as if they were satellites tethered to an invisible path.

This is where GSAP's MotionPathPlugin came in.

Building the Path

Every orbit needs a path. In this case, it's just a perfect circle defined in SVG:

<svg
  width="760"
  height="760"
  viewBox="0 0 760 760"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
>
  <path
    ref={animatePathRef}
    id="orbit-path"
    opacity="0.2"
    d="M379.922 0.631836C589.435 0.631836 759.369 170.407 759.369 380C759.369 589.513 589.435 759.447 379.922 759.447C170.408 759.368 0.632812 589.513 0.632812 379.921C0.632812 170.407 170.408 0.631836 379.922 0.631836Z"
    stroke="white"
    strokeOpacity="0.3"
    strokeWidth="2"
  />
</svg>

This is just math: A circle centered at (380, 380) with a radius of ~379px.

This invisible circle is what drives all the animations.

GSAP MotionPathPlugin

Now the fun part: GSAP's MotionPathPlugin.

The plugin allows us to map any element to follow an SVG path. No need to calculate angles, positions, or Bézier curves manually, GSAP does all the geometry.

First, I registered the plugin and created a looping timeline:

gsap.registerPlugin(MotionPathPlugin);

const tl = gsap.timeline({
  repeat: -1,   // infinite loop
  yoyo: true,   // goes forward, then backward
  defaults: {
    duration: 90,
    ease: "none", // constant speed
  },
});

Notice the duration: 90s. That's intentionally slow, almost meditative. I didn't want jittery or distracting motion.

Aligning Elements

Before animating, all icons are centered with xPercent / yPercent and transformOrigin:

tl.set(
  [
    profileRef.current,
    awsRef.current,
    nextjsRef.current,
    reactRef.current,
    githubRef.current,
    typescriptRef.current
  ],
  {
    xPercent: -50,
    yPercent: -50,
    transformOrigin: "50% 50%",
  }
);

This ensures the icons rotate around their own centers instead of offsetting awkwardly.

Offsetting Each Orbit

If all elements started at the same position, they'd overlap. So each one gets a different start and end offset along the path.

tl.to(profileRef.current, {
  motionPath: {
    path: animatePathRef.current,
    align: animatePathRef.current,
    autoRotate: false,
    start: 0,
    end: 0.5,
  },
 }, 0);

tl.to(awsRef.current, {
  motionPath: {
    path: animatePathRef.current,
    start: 0.18,
    end: 0.65,
    },
}, 0);

tl.to(reactRef.current,
    {
  motionPath: {
    path: animatePathRef.current,
    start: 0.32,
    end: 0.82
  },
}, 0);

tl.to(githubRef.current, {
  motionPath: {
    path: animatePathRef.current,
    start: 0.5,
    end: 1,
  },
}, 0);

tl.to(nextjsRef.current, {
  motionPath: {
    path: animatePathRef.current,
    start: 0.66,
    end: 1.178,
  },
}, 0);

tl.to(typescriptRef.current, {
  motionPath: {
    path: animatePathRef.current,
    start: 1.8,
    end: 2.33,
  },
}, 0);

Each start / end value represents a percentage along the orbit. By staggering them, I created natural spacing so icons feel like independent satellites.

The Visual Effect

When combined, the result is:

  • Icons slowly orbit around the gradient mesh
  • They move at the same speed, but are offset in position
  • The yoyo: true timeline makes them oscillate back and forth

The mesh background stays static while the orbiting creates motion contrast. It feels alive, but not chaotic.

Design Considerations

Subtlety over Flashiness

  • Motion should enhance, not overwhelm.
  • That's why the animation runs for 90s per loop.

Circular Path vs Random Path

  • I tested irregular Bézier paths, but they felt distracting.
  • The circle creates harmony and predictability.

Layering Icons

  • Different icons overlap naturally at times.
  • This adds depth, almost like orbital parallax.

Balance with Static Mesh

  • The mesh itself doesn't move.
  • This contrast makes the animation stand out more.

Key Takeaways

  • GSAP MotionPathPlugin is perfect for path-based animations without heavy math.
  • Using different path offsets makes multiple elements move together without collisions.
  • Slow, looping motion feels polished and professional compared to fast spins.
  • Static backgrounds + dynamic motion = balanced design.

Total likes

0 likes

Share

Animating a Gradient Mesh with GSAP Motion Paths — Ugi Stelmokaitis