Gradient Along SVG Path with GSAP
Tags: tutorial, svg, gsap, javascript
Published: 2023-05-30 11:04:24

If you've ever come across, you know what I'm talking about. It's really annoying to deal with this type of gradient in SVG.


![gradient along SVG path preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f2cvmfdc1ympvgvsck89.png)

Check out the final codepen or follow the step-by-step process.

{% codepen https://codepen.io/ksenia-k/pen/mdzYKmm %}



## Native SVG methods

SVG `<path>`, as well as other SVG strokes like `<polygon>` and `<polyline>`, can only be colored using `linearGradient` and `radialGradient`.

```html
<!-- Here and below we're using the same pre-defined path -->
<svg width="0" height="0" viewBox="0 0 250 250">
    <defs>
        <path id="gradient-path" d="M36.5,91.2C-7.5,185.5,99.3,224.4,170,203.1c55-16.6,57.8-87.4,1.6-104C71,69.5,9.4,207.7,46,228.6c62.7,35.8,189.7-116,133-211"/>
    </defs>
</svg>

<!-- Coloring the path with native SVG gradient -->
<svg id="demo" viewBox="0 0 500 250">
    <defs>
        <linearGradient id="linear-grad">
            <stop offset="0" stop-color="#f7ff00"/>
            <stop offset="1" stop-color="#db36a4"/>
        </linearGradient>
        <radialGradient id="radial-grad">
            <stop offset="0" stop-color="#f7ff00"/>
            <stop offset="1" stop-color="#db36a4"/>
        </radialGradient>
    </defs>
    <use xlink:href="#gradient-path" stroke="url(#linear-grad)" fill="none" stroke-width="15"/>
    <use xlink:href="#gradient-path" stroke="url(#radial-grad)" x="250" fill="none" stroke-width="15"/>
</svg>
```

While both gradient types have [lots of settings](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Gradients), the color distribution always follows a straight line.

![native gradients preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/excghq9iub5trggs1j90.png)

If we want the color to change along the curve, we'll need a help from CSS or JavaScript. It’s a common problem and we have some solutions. Mike Bostock, the creator of d3.js library, has shared [this](https://gist.github.com/mbostock/4163057) approach. Patrick Cason created a [small library](https://github.com/cereallarceny/gradient-path) based on Mike solution but without d3.js dependency. Other folks have covered more specific cases. For example, Amit Sheen posted about a CSS-only [trick](https://css-tricks.com/my-struggle-to-use-and-animate-a-conic-gradient-in-svg/) to build and animate the gradient along a circular path. 

--------------------------------

## Creating a Gradient with GSAP

As a ~~big fan~~ frequent user of GSAP, I’d also like to contribute.

We'll use GSAP and their MotionPathPlugin to distribute `<circle>` elements along the given `<path>` and color those circles to compose the gradient.

After defining a reference `<path>` and adding [both GSAP files](https://greensock.com/docs/v3/Installation?checked=core,motionPath), we create <circle> elements, position them along the path, and apply gradient color to them.

```html
<svg viewBox="0 0 250 250">
    <defs>
        <path id="gradient-path" d="M36.5,91.2C-7.5,185.5,99.3,224.4,170,203.1c55-16.6,57.8-87.4,1.6-104C71,69.5,9.4,207.7,46,228.6c62.7,35.8,189.7-116,133-211"/>
    </defs>
    <g class="dots">
        // to hold all the circles
    </g>
</svg>
```

```js
const strokeWidth = 15;
const colors = ["#f7ff00", "#db36a4"];
const gradientPath = document.querySelector("#gradient-path");
// const numberOfDots = Math.ceil(gradientPath.getTotalLength() / strokeWidth); // for circles to be placed back-to-back
const dotsDensity = .5 * strokeWidth;
const numberOfDots = Math.ceil(dotsDensity * gradientPath.getTotalLength() / strokeWidth);
```


To find out the number of circles we can use `getTotalLength()`. This native JS method retrieves the total length of the path. If we take `gradientPath.getTotalLength() / strokeWidth` as a number of dots, they stay back to back. By increasing the `dotsDensity` we can get a smooth line:

![gsap gradients along the path](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ez177jcrm8mu2rs7fn2v.png)

We create the circles using the native JS `appendChild` method, and define the circle's position with the [motionPath plugin](https://greensock.com/docs/v3/Plugins/MotionPathPlugin). 

```js
const dotsGroup = document.querySelector(".dots");
createBasicGradient(dotsGroup);

function createBasicGradient(g) {
    for (let idx = 0; idx < numberOfDots; idx++) {
        const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
        g.appendChild(circle);

        gsap.set(circle, {
            motionPath:
                path: gradientPath, // the target path
                start: idx / numberOfDots, // the position on target path
                end: idx / numberOfDots,
            },
            attr: {
                cx: 0, // the position is defined by transform attribute, so we keep circle center on (0, 0) point
                cy: 0,
                r: .5 * strokeWidth, // to compose strokeWidth
                fill: gsap.utils.interpolate(colors, (idx / numberOfDots)) // linear interpolation between 2 (or more!) given colors
            }
        });
    }
}
```


> ✏️ **Tip #1**
> I use simple linear interpolation between two colors with the GSAP `interpolate` utility. You can easily add more colors to the `colors` array, or build a custom function to calculate the color from `(idx / numberOfDots)` value.


We might need to fix the stroke tips, unless we're good with round linecap.

![Masked and unmasked gradient](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e7vpfou0m8pb9mh300s6.png)

There're different ways to do so. For example, we can mask the dots with the original path.

```html
<path id="gradient-path" d="M36.5,91.2C-7.5,185.5,99.3,224.4,170,203.1c55-16.6,57.8-87.4,1.6-104C71,69.5,9.4,207.7,46,228.6c62.7,35.8,189.7-116,133-211"/>
<mask id="gradient-path-clip">
    <use xlink:href="#gradient-path" stroke-width="15" fill="none" stroke="white"/>
</mask>
...
<g mask="url(#gradient-path-clip)" class="dots">
</g>
```

The basic gradient along the stroke is done! 😎

> ✏️ **Tip #2**
> You can easily replace `<circle>` with another shape and compose a gradient with rectangles or something else. If doing so, you additionally align the particles along the path:
> ```js
    motionPath: {
        ...
        align: gradientPath,
        alignOrigin: [.5, .5],
        ...
    }
```

> ✏️ **Tip #3**
> If you're masking the circles with the original path and the path doesn't intersect itself, you can increase the dot radius and decrease the dots density. It's much better for performance.
> ![increased radius size](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yxn48oe8rogfge7fqzv5.gif)


--------------------------------

## Advanced techniques
### Handling stroke intersections

Let's say we want our path to look like a proper knot with the stroke going "under" itself at the bottom. 
We can reorder the dots and append the "back" dots before others. Then we apply the color and position using remapped index instead the original order.

![remapped](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xbz7farpxtp2hgkhf4m3.png)

```js
for (let idx = 0; idx < numberOfDots; idx++) {
    // append circles in normal order
    const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
    g.appendChild(circle);

    // remap the index to move the some of the back dots to the middle of stroke length
    let idxRemapped = idx;
    if (idx < .1 * numberOfDots) {
        idxRemapped += Math.ceil(.7 * numberOfDots); // [ .0 .. .1 ] to [ .7 .. .8 ]
    } else {
        if (idx < .8 * numberOfDots) {
            idxRemapped -= Math.ceil(.1 * numberOfDots); // [ .1 .. .8 ] to [ .0 .. .7 ]
        }
    }

    // apply position and color using idxRemapped
    gsap.set(circle, {
        // ...
    });
}
```
The index remapping is specific for particular path but you hopefully got the idea :)

### Varying Stroke Width

Building dynamic path width is pretty straightforward, we just need to change the dot size according to the index. 

![dynamic path width](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2avi0cpjxok68kvp8l2c.png)

```js
gsap.set(circle, {
    attr: {
        // ...
        r: .5 * strokeWidth + .02 * idx,
    }
})
```

And, of course, we can use more fancy function to calculate the dot size.

### Animate the gradient

GSAP is an animation platform, and the MotionPathPlugin was designed to _move_ things along a path. So, rather than setting the dot position, we can easily animate it. 

![animated gradients](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g664txdhz81q28ec3vy3.gif)

```js
for (let idx = 0; idx < numberOfDots; idx++) {
    // create dot and set static attributes like before
    const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
    g.appendChild(circle);
    gsap.set(circle, {
        // motionPath: {
        //     path: gradientPath,
        //     start: idxRemapped / numberOfDots,
        //     end: idxRemapped / numberOfDots,
        // },
        attr: {
            cx: 0,
            cy: 0,
            r: .5 * strokeWidth,
            fill: gsap.utils.interpolate(colors, (idxRemapped / numberOfDots))
        }
    });
    
    // add position-on-path animation
    gsap.to(circle, {
        motionPath: {
            path: gradientPath // position along the path
        },
        duration: 2, // the time each dot takes to travel the whole path
        ease: "none",
        repeat: -1 // loop the animation
    }).progress(idx / numberOfDots); // each dot start moving from their own position
}
```


It's easy to combine gradient animation with other effects. For some cases you may want to increase the dots density or add masking. 

--------------------------------

And there you have it! 🎉
I gathered all the examples on the single codepen, subscribe for updates!

{% codepen https://codepen.io/ksenia-k/pen/mdzYKmm %}