Gradient Along SVG Path with GSAP
Ksenia Kondrashova
Posted on May 30, 2023
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.
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>
While both gradient types have lots of settings, the color distribution always follows a straight line.
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>
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:
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
}
});
}
}
βοΈ Tip #1
I use simple linear interpolation between two colors with the GSAPinterpolate
utility. You can easily add more colors to thecolors
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.
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>
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.
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.
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.
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.
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!
Posted on May 30, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.