I want to create a CSS animation where an arrow moves along an SVG snake path while scrolling, and changes icon on every new section.
Here is my page design for reference.
I looked at a few examples:
But I'm unable to recreate the intended behavior with my page design.
Here is my code so far:
window.addEventListener('scroll', function() {
let l = Path_440.getTotalLength();
let dasharray = l;
let dashoffset = l;
e = document.documentElement;
theFill.setAttributeNS(null, "stroke-dasharray", l);
theFill.setAttributeNS(null, "stroke-dashoffset", l);
dashoffset = l - window.scrollY * l / (e.scrollHeight - e.clientHeight);
//console.log('window.scrollY', window.scrollY, 'scrollTop', e.scrollTop, 'scrollHeight', e.scrollHeight, 'clientHeight', e.clientHeight, 'dash-offset', dashoffset);
theFill.setAttributeNS(null, "stroke-dashoffset", dashoffset);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<svg width="246" height="2990" viewBox="0 0 246 2990" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<path id="Path_440" d="M210.001 1.5C210.001 1.5 41.0015 324.5 6.50082 617.5C-27.004 902.042 182.501 1032.5 240.001 1313C275.095 1484.2 29.8527 1661 41.0008 1914.5C50.4751 2129.94 230.431 2237.5 235.001 2431.5C240.42 2661.59 41.0008 2988 41.0008 2988" stroke="#F39029" stroke-width="4" stroke-dasharray="20 10"/>
</defs>
<use xlink:href="#Path_440" stroke="#000" stroke-width="4" stroke-dasharray="1"/>
<use id="theFill" xlink:href="#Path_440" stroke="#000" stroke-width="1"/>
</svg>
The modern strategy is to use CSS motion paths (a.k.a offset-path
and path()
in CSS). By calculating the scrollProgress
of the SVG path, you can use that to calculate the offset-distance
for your icon as a percentage. Just make sure the icon is positioned absolutely at your SVG's (0,0)
.
In the simple case that the SVG path spans the whole page, scrollProgress
is just window.scrollY / (docElt.scrollHeight - docElt.clientHeight)
. However, if the SVG path does not span the whole scroll container, then you need to use getBoundingClientRect()
to calculate the scrollProgress
and clamp it between 0 and 1. The code demo below covers this more complex case.
To fill the path in as the user scrolls, combine the getTotalLength()
method of path elements with scrollProgress
to compute how far the user is along the path in pixels (drawLength
) and how much they have left in pixels (rest
). Then, you can use a common trick with stroke-dasharray
which is to set it to ${drawLength}px ${rest}px
.
Finally, you can also use scrollProgress
to determine which icon to display with CSS background-image
. Use offset-rotate: 0rad;
to prevent the icon from rotating in the direction of the path.
pathIcon.style.offsetPath = `path('${Path_440.getAttribute("d")}')`;
const pathLength = Path_440.getTotalLength();
function clamp(min, val, max) {
return Math.min(Math.max(min, val), max);
}
function updatePath() {
const docElt = document.documentElement;
const pathBox = theFill.getBoundingClientRect();
// calculates scroll progress based on viewport progress
const scrollProgress =
clamp(0, -pathBox.y / (pathBox.height - docElt.clientHeight), 1);
pathIcon.style.offsetDistance = `${scrollProgress * 100}%`;
// These lines fill in the dashes as you scroll down.
const drawLength = pathLength * scrollProgress;
const rest = pathLength - drawLength;
theFill.style.strokeDasharray = `${drawLength}px ${rest}px`;
// You can update the icon/SVG here using your own logic.
// For the example, I'm changing the CSS background-image.
pathIcon.style.backgroundImage = `url(${getIconSrc(scrollProgress)})`;
}
function getIconSrc(scrollPercent) {
if (scrollPercent < 0.2) {
return 'https://via.placeholder.com/25x25/FF0000?text=red';
} else if (scrollPercent < 0.4) {
return 'https://via.placeholder.com/25x25/FFA500?text=orange';
} else if (scrollPercent < 0.6) {
return 'https://via.placeholder.com/25x25/FFFF00?text=yellow';
} else if (scrollPercent < 0.8) {
return 'https://via.placeholder.com/25x25/00FF00?text=green';
} else if (scrollPercent < 1) {
return 'https://via.placeholder.com/25x25/0000FF?text=blue';
} else if (scrollPercent === 1) {
// A scrollPercent of 1 indicates that we have reached the end of the path.
return 'https://via.placeholder.com/25x25/A020F0?text=purple';
}
}
updatePath();
window.addEventListener("scroll", () => updatePath());
#pathIcon {
position: absolute;
inset: 0;
width: 25px;
height: 25px;
background-size: 25px;
offset-rotate: 0rad;
}
<div style="height: 175px;"></div>
<div style="position: relative;">
<svg width="246" height="2990" viewBox="0 0 246 2990" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<path id="Path_440"
d="M210.001 1.5C210.001 1.5 41.0015 324.5 6.50082 617.5C-27.004 902.042 182.501 1032.5 240.001 1313C275.095 1484.2 29.8527 1661 41.0008 1914.5C50.4751 2129.94 230.431 2237.5 235.001 2431.5C240.42 2661.59 41.0008 2988 41.0008 2988"
stroke-width="4" stroke="#F39029" />
</defs>
<use href="#Path_440" stroke-dasharray="20 10" />
<use id="theFill" href="#Path_440" />
</svg>
<div id="pathIcon"></div>
</div>
Hopefully, in the near future, we will get scroll-driven animations and even better support for CSS motion paths in browsers, which will make animating an element along a path significantly simpler with CSS alone.
For instance, if you are on Chrome Canary 115+ with the experimental-web-platform-features
flag enabled, the following CSS works just as well if not better. No JS needed:
#arrow {
offset-path: url(#Path_440);
offset-anchor: left;
animation: offsetDistance linear;
animation-timeline: scroll();
}
@keyframes offsetDistance {
from {
offset-distance: 0%;
}
to {
offset-distance: 100%;
}
}
<svg width="246" height="2990" viewBox="0 0 246 2990" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<path id="Path_440"
d="M210.001 1.5C210.001 1.5 41.0015 324.5 6.50082 617.5C-27.004 902.042 182.501 1032.5 240.001 1313C275.095 1484.2 29.8527 1661 41.0008 1914.5C50.4751 2129.94 230.431 2237.5 235.001 2431.5C240.42 2661.59 41.0008 2988 41.0008 2988"
stroke-width="4" stroke="#F39029" />
</defs>
<use xlink:href="#Path_440" stroke-dasharray="20 10" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" id="arrow" style="position: absolute; left: 0; top: 0;" fill-rule="evenodd" clip-rule="evenodd">
<path d="M21.883 12l-7.527 6.235.644.765 9-7.521-9-7.479-.645.764 7.529 6.236h-21.884v1h21.883z" />
</svg>
Learn more about how to use scroll-driven animations.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With