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));