DEV Community

Cover image for Gradient Along SVG Path with GSAP
Ksenia Kondrashova
Ksenia Kondrashova

Posted on

Gradient Along SVG Path with GSAP

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

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

Native SVG methods

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

<!-- 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>
Enter fullscreen mode Exit fullscreen mode

While both gradient types have lots of settings, the color distribution always follows a straight line.

native gradients preview

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 approach. Patrick Cason created a small library 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 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, we create elements, position them along the path, and apply gradient color to them.

<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>
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode

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

We create the circles using the native JS appendChild method, and define the circle's position with the motionPath plugin.

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
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

✏️ 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

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

<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>
Enter fullscreen mode Exit fullscreen mode

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:

    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


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

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, {
        // ...
    });
}
Enter fullscreen mode Exit fullscreen mode

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

gsap.set(circle, {
    attr: {
        // ...
        r: .5 * strokeWidth + .02 * idx,
    }
})
Enter fullscreen mode Exit fullscreen mode

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

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
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (3)

Collapse
 
erinposting profile image
Erin Bensinger

This is so cool!! Thanks for sharing!

Collapse
 
blint profile image
Ibrohim Hanafi

thanks that tutorials

Collapse
 
tainasimoes profile image
Tainá Simões

Perfect <3