JSFiddle - React, Tailwind, and code Playground
by RAX7
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<div class="services">
<svg class="services__line" id="svg-line" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 815 680" preserveAspectRatio="none">
<path id="path-line" vector-effect="non-scaling-stroke" fill="none" stroke="red" stroke-width="2" d="M611,9c38,21,109,58,109,99s-66.4,55.5-350,51c-281.5-4.5-317.5,6.5-317.5,42.5s53,63,355,63,355-34,374,12-71,101-243,101-295-55-455-55c-17,0-60-1-60,38s111,47,181,60,163,11,195,249"/>
</svg>
<ul class="services__list">
<li class="services__item" data-length-offset="0">1 Lorem ipsum dolor.</li>
<li class="services__item" data-length-offset="15">2 Lorem ipsum dolor.</li>
<li class="services__item" data-length-offset="25">3 Lorem ipsum dolor.</li>
<li class="services__item" data-length-offset="35">4 Lorem ipsum dolor.</li>
<li class="services__item" data-length-offset="45">5 Lorem ipsum dolor.</li>
<li class="services__item" data-length-offset="63">6 Lorem ipsum dolor.</li>
<li class="services__item" data-length-offset="80">7 Lorem ipsum dolor.</li>
<li class="services__item" data-length-offset="94">8 Lorem ipsum dolor.</li>
</ul>
</div>
CSS
.services {
position: relative;
height: 600px;
}
.services__line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.services__list {
position: relative;
overflow-x: hidden;
padding: 0;
margin: 0;
list-style: none;
width: 100%;
height: 100%;
}
.services__item {
position: absolute;
left: 0;
top: 0;
max-width: 100px;
padding: 5px;
transition: transform 250ms linear;
background-color: rgba(0, 255, 0, 0.5);
}
.services__item::before {
content: '';
position: absolute;
width: 10px;
height: 10px;
left: -5px;
top: -5px;
border-radius: 50%;
background-color: rgba(0, 0, 255, 0.5);
}
JavaScript
const svgLine = document.querySelector('#svg-line');
const pathLine = document.querySelector('#path-line');
const itemsEls = document.querySelectorAll('.services__item[data-length-offset]');
const pathLineTl = pathLine.getTotalLength();
const items = [...itemsEls]
.map(el => {
const offset = parseFloat(el.dataset.lengthOffset);
const coords = pathLine.getPointAtLength(offset / 100 * pathLineTl);
return {el, coords};
});
function updateItemsPosition(event) {
const boundingRect = svgLine.getBoundingClientRect();
const point = svgLine.createSVGPoint();
const ctm = svgLine.getScreenCTM();
items.forEach(({el, coords}) => {
point.x = coords.x;
point.y = coords.y;
coords = point.matrixTransform(ctm);
coords.x = coords.x - boundingRect.x + window.scrollX;
coords.y = coords.y - boundingRect.y + window.scrollY;
el.style.transform = `translate(${coords.x}px, ${coords.y}px)`;
});
}
updateItemsPosition();
window.addEventListener('resize', _.debounce(updateItemsPosition, 250));