Как можно сверстать подобный блок последовательных пунктов roadmap на HTML и CSS?

Допустим есть вот такой Блок последовательных пунктов. Как его можно сверстать адаптивно на чистом HTML и CSS? На флексбоксах такое реализуемо или подобное делается по другому?


Ответы (2 шт):

Автор решения: Laukhin Andrey

Основная проблема получить универсальное решение при помощи только HTML и CSS заключается в порядке следования элементов. Блоки следуют друг за другом в едином потоке, а при разрыве прыгают вниз. В нашем же случае, каждая четная образуемая строка должна иметь обратный порядок элементов. И нет никаких способов выделить такую строку и в рамках нее изменить порядок. А если мы обернем строки в группы, то мы лишимся адаптивности, т.к. структура элементов будет фиксирована.

Первый подход, который мы рассмотрим, это частное решение задачи, где мы вручную будем задавать порядок и стилизовать каждый элемент для всех @media. Для организации структуры будем использовать Grid.

Имеем семь блоков. При выводе в две колонки нам нужна такая последовательность:
1 2 4 3 5 6 8 7

Восьмой блок – пустышка, чтобы правильно расставить блоки в Grid.
Для четырех колонок: 1 2 3 4 8 7 6 5

У Flex/Grid есть свойство order. Задать порядок нужному элементу можно так:

.block:nth-child(1) { order: 1; }
.block:nth-child(2) { order: 2; }
…

Для трех контрольных точек @media получим следующее:

.chain {
  display: grid;
  gap: 10px;
}

.block {
  height: 100px;
  background: yellow;
  font-size: 200%;
}

/* 2 rows */
@media (min-width: 500px) and (max-width: 749px) {
  .chain { grid-template-columns: 1fr 1fr; }

  .block:nth-child(1) { order: 1; }
  .block:nth-child(2) { order: 2; }
  .block:nth-child(3) { order: 4; }
  .block:nth-child(4) { order: 3; }
  .block:nth-child(5) { order: 5; }
  .block:nth-child(6) { order: 6; }
  .block:nth-child(7) { order: 8; }
  .empty              { order: 7; }
}

/* 4 rows */
@media (min-width: 750px) {
  .chain { grid-template-columns: 1fr 1fr 1fr 1fr; }
  .block:nth-child(1) { order: 1; }
  .block:nth-child(2) { order: 2; }
  .block:nth-child(3) { order: 3; }
  .block:nth-child(4) { order: 4; }
  .block:nth-child(5) { order: 8; }
  .block:nth-child(6) { order: 7; }
  .block:nth-child(7) { order: 6; }
  .empty              { order: 5; }
}
<div class="chain">
  <div class="block">1</div>
  <div class="block">2</div>
  <div class="block">3</div>
  <div class="block">4</div>
  <div class="block">5</div>
  <div class="block">6</div>
  <div class="block">7</div>
  <div class="empty"></div>
</div>

Стилизация бордюрами всего этого добра – то еще удовольствие.
Добавим стили под конкретную структуру (приведенную в вопросе):

:root {
  --color: #3f48cc;
  --line-size: 4px;
  --line-style: solid;
  --border: var(--line-size) var(--line-style) var(--color);
  --dot-offset: 10px;
  
  font-family: arial;
}

.chain { 
  display: grid;
  color: var(--color);
  padding-top: 40px;
}

.block { padding: 50px 0; }
.empty { order: 999; }

.block.first { margin-left: 10px; }

.num {
  width: 20px;
  margin-top: -87px;
  margin-left: -2px;
  padding-bottom: 6px;
  border-bottom: 20px solid;
}

.block h2 {
  font-size: 120%;
  font-weight: normal;
  margin-top: 0.3em;
}

.block p { font-size: 80%;  }


/* 2 rows */
@media (min-width: 500px) and (max-width: 749px) {
  .chain { grid-template-columns: 1fr 1fr; }

  .block:nth-child(1) { order: 1; }
  .block:nth-child(2) { order: 2; }
  .block:nth-child(3) { order: 4; }
  .block:nth-child(4) { order: 3; }
  .block:nth-child(5) { order: 5; }
  .block:nth-child(6) { order: 6; }
  .block:nth-child(7) { order: 8; }
  .empty              { order: 7; }

  .block:nth-child(4n):nth-child(4n+1) {
    background: red;
  }

  .block:nth-child(4n+2) { border-right: var(--border); }
  .block:nth-child(4n):not(:last-child) { 
    border-left: var(--border);
    padding-left: calc(var(--dot-offset) - var(--line-size));
  }
  
  .block:nth-child(4n):not(:last-child) + .block {
    padding-left: var(--dot-offset);
  }
  
  .block:not(:last-child) { border-top: var(--border); }
}

/* 4 rows */
@media (min-width: 750px) {
  .chain { grid-template-columns: 1fr 1fr 1fr 1fr; }

  .block:nth-child(1) { order: 1; }
  .block:nth-child(2) { order: 2; }
  .block:nth-child(3) { order: 3; }
  .block:nth-child(4) { order: 4; }
  .block:nth-child(5) { order: 8; }
  .block:nth-child(6) { order: 7; }
  .block:nth-child(7) { order: 6; }
  .empty              { order: 5; }
  
  .block:nth-child(8n+4) { border-right: var(--border); }
  .block:nth-child(-n+7) { border-top: var(--border); }
}

/* 1 row */
@media (max-width: 499px)
{
  .chain { padding: 0; }

  .block:nth-child(-n+6) { border-left: var(--border); }
  .block:nth-child(7) {
    margin-left: calc(var(--line-size) + var(--dot-offset));
  }
  
  .block {
    padding: 0 0 20px 10px;
    margin-left: 10px;
  }

  .num {
    margin: 0px 0 0 -22px;
    padding: 0 0 0 5px;
    border-bottom: none;
    border-left: 20px solid;
  }
}
<div class="chain">
  <div class="block first">
    <div class="num">1.</div>
    <h2>Регистрация</h2>
  </div>
  <div class="block">
    <div class="num">2.</div>
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="block">
    <div class="num">3.</div>
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="block">
    <div class="num">4.</div>
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="block">
    <div class="num">5.</div>
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="block">
    <div class="num">6.</div>
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="block">
    <div class="num">7.</div>
    <h2>Сказке конец</h2>
    <p>Описание</p>
  </div>
  <div class="empty"></div>
</div>

Плюсы: негромоздкий CSS, несложный скрипт JS делает решение универсальным.
Минусы: негибкое решение, для каждого набора и макета нужно колдовать CSS.

Второй подход. Каждую пару элементов пакуем в Grid. Каждую пару таких гридов пакуем еще в один Grid. Полученные пары добавляем в базовый Grid-контейнер, сколько нужно. Структура:

<chain>
    <pair1>
        <!-- только два узла внутри -->
        <pair2>
            <!-- только два элемента внутри -->
            <block/>
            <block/>
        </pair2>
        <pair2>
            <block/>
            <block/>
        </pair2>
    </pair1>
    ...
</chain>

Если pair1 и pair2 - одноколоночные гриды, то все элементы выведутся списком.
Если pair2 - двухколоночный грид, то получим макет с двумя колонками, где в каждом нечетном pair2 нужно поменять местами элементы.
Если pair1 и pair2 - двухколоночные гриды, то получим макет с четырьмя колонками, где в нечетных pair1 (и во вложенных pair2) меняем местами элементы. И т.д.

Таким образом, мы можем организовывать эту структуру в макеты, с кратным степени 2 числом колонок (1, 2, 4, 8, …):

.chain, .pair1, .pair2 {
  display: grid;
  gap: 10px;
}

.block { 
  height: 100px;
  background: yellow;
  font-size: 200%;
}

@media (min-width: 500px) and (max-width: 749px) {

  .pair2 { grid-template-columns: 1fr 1fr; }  
  .pair2:nth-child(even) > :first-child { order: 1; }
}

@media (min-width: 750px) {

  .pair1, .pair2 { grid-template-columns: 1fr 1fr; }
  .pair1:nth-child(even) :nth-child(1) { order: 1; }
}
<div class="chain">
  <div class="pair1">
    <div class="pair2">
      <div class="block">1</div>
      <div class="block">2</div>
    </div>
    <div class="pair2">
      <div class="block">3</div>
      <div class="block">4</div>
    </div>
  </div>
  <div class="pair1">
    <div class="pair2">
      <div class="block">5</div>
      <div class="block">6</div>
    </div>
    <div class="pair2">
      <div class="block">7</div>
      <div></div>
    </div>
  </div>
</div>

Достаточно лаконичный CSS, где нам не требуется вручную задавать порядок каждого элемента, потому что есть селекторы even/odd.

А вот стилизация убивает всю лаконичность:

:root {
  --color: #3f48cc;
  --line-size: 4px;
  --line-style: solid;
  --border: var(--line-size) var(--line-style) var(--color);
  --dot-offset: 10px;
  
  font-family: arial;
}

.chain, .pair1, .pair2 { display: grid; position: relative; }
.chain { color: var(--color); padding-top: 40px; }

.chain {
  overflow: hidden;
}

.block { 
  padding: 50px 0;
  width: 100%;
  margin-left: var(--dot-offset);
}

.block.first { margin-left: 10px; }

.num {
  width: 20px;
  margin-top: -87px;
  margin-left: -2px;
  padding-bottom: 6px;
  border-bottom: 20px solid;
}

.block h2 {
  font-size: 120%;
  font-weight: normal;
  margin-top: 0.3em;
}

.block p { font-size: 80%;  }


/*******/
@media (min-width: 500px) and (max-width: 749px) {

  .pair2 { grid-template-columns: 1fr 1fr; }  
  .pair2:nth-child(even) > :first-child { order: 1; }
    
  .pair1:not(:last-child) > :last-child:after {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: calc(-1 * var(--line-size));
    width: var(--dot-offset);
    border: var(--border);
    border-right: none;
  }
  
  .pair2:not(:last-child)  { border-right: var(--border); }
  .pair2:not(:first-child) { margin-right: var(--line-size); }
  
  .pair1:last-child >
  .pair2:last-child:nth-child(odd) >
  .block.last
  {
    border: none;
    margin-top: var(--line-size);
  }    
  
  .block { border-top: var(--border);  }
}

@media (min-width: 750px) {

  .pair1, .pair2 { grid-template-columns: 1fr 1fr; }
  .pair1:nth-child(even) :nth-child(1) { order: 1; }
  
  .pair1:nth-child(even):not(:last-child):after {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: calc(-1 * var(--line-size));
    width: var(--dot-offset);
    border: var(--border);
    border-right: none;
  }  
  .pair1:nth-child(odd ):not(:last-child) { border-right: var(--border); }
  .pair1:nth-child(even):not(:last-child) { margin-right: var(--line-size); }
  
  .pair1:last-child:nth-child(odd) .block.last
  {
    border: none;
    margin-top: var(--line-size);
  }    
  
  .block { border-top: var(--border); }
}

@media (max-width: 499px)
{
  
  .chain { padding: 0; }
  
  .block { 
    border-left: var(--border);
    padding: 0 0 20px 10px;
  }

  .num {
    margin: 0px 0 0 -22px;
    padding: 0 0 0 5px;
    border-bottom: none;
    border-left: 20px solid;
  }
  
  .block.last
  {
    border: none;
    margin-left: calc(var(--line-size) + var(--dot-offset));
  }
}
<div class="chain">
  <div class="pair1">
    <div class="pair2">
      <div class="block first">
        <div class="num">1.</div>
        <h2>Регистрация</h2>
      </div>
      <div class="block">
        <div class="num">2.</div>
        <h2>Следующий этап</h2>
        <p>Описание</p>
      </div>
    </div>
    <div class="pair2">
      <div class="block">
        <div class="num">3.</div>
        <h2>Следующий этап</h2>
        <p>Описание</p>
      </div>
      <div class="block">
        <div class="num">4.</div>
        <h2>Следующий этап</h2>
        <p>Описание</p>
      </div>
    </div>
  </div>
  <div class="pair1">
    <div class="pair2">
      <div class="block">
        <div class="num">5.</div>
        <h2>Следующий этап</h2>
        <p>Описание</p>
      </div>
      <div class="block">
        <div class="num">6.</div>
        <h2>Следующий этап</h2>
        <p>Описание</p>
      </div>
    </div>
    <div class="pair2">
      <div class="block">
        <div class="num">7.</div>
        <h2>Следующий этап</h2>
        <p>Описание</p>
      </div>
      <div class="block">
        <div class="num">8.</div>
        <h2>Следующий этап</h2>
        <p>Описание</p>
      </div>
    </div>
  </div>
  <div class="pair1">
    <div class="pair2">
      <div class="block">
        <div class="num">9.</div>
        <h2>Следующий этап</h2>
        <p>Описание</p>
      </div>
      <div class="block">
        <div class="num">10.</div>
        <h2>Следующий этап</h2>
        <p>Описание</p>
      </div>
    </div>
    <div class="pair2">
      <div class="block last">
        <div class="num">11.</div>
        <h2>Приехали</h2>
        <p>Описание</p>
      </div>
      <div></div>
    </div>
  </div>
</div>

Первый .block должен быть .first, последний – .last. Если в паре только один элемент, то добавляем пустой div.

Плюсы: гибкое решение, можно добавить сколько угодно элементов, не трогая CSS.
Минусы: число колонок макета кратно степени 2. Более сложная структура узлов.

Итог изысканий: я бы использовал первый подход + скрипт JS с подпиской на MediaQueryListener.
UPD. Добавил еще одно решение отдельным ответом.

→ Ссылка
Автор решения: Laukhin Andrey

Ранее я опубликовал ответ с двумя подходами. Третий подход решил опубликовать отдельно.

Предыдущие варианты имели свои минусы, и я нашел более оптимальный. Наша задача – построить дорожную карту (roadmap) связанных элементов, организованных змейкой, с любым количеством элементов, без необходимости вручную прописывать CSS-правила для задания порядка каждого элемента. При этом, структура должна быть адаптивной, с любой конфигурацией макета (кол-во колонок) контрольных точек @media, и без JS.

Все звенья дорожной карты зададим простым линейным списком элементов, при этом каждому укажем CSS-переменную с номером:

<div class="link" style="--num: 1"></div>
<div class="link" style="--num: 2"></div>
<div class="link" style="--num: 3"></div>
...

Предположим, в нашем макете 4 колонки. Список элементов по номерам:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

Выделенные элементы – это четные строки змейки, элементы которой нужно менять местами. Эти группы элементов можно получить при помощи комбинации псевдоклассов :nth-child(An+B).

А вот изменение порядка следования элементов в группе и есть основная арифметическая хитрость подхода. Если для группы 5 6 7 8 сложить первый и последний элемент, получим 13. Теперь последовательно вычтем номера элементов из 13, и получим 8 7 6 5. Однако, внутри CSS-правила нам недоступен номер первого и последнего элемента в группе (строке), а только текущий --num. Но если немного поиграть с цифрами, зная число колонок --cols, можно получить такую формулу:

--base = --cols * (1 + 2 * F) + 1, где F = floor((--num - 1) / --cols)

Рассчитав для каждого элемента значение --base, получим:

--num    1 2 3 4 5  6  7  8  9  10 11 12 13 14 15 16 …
--base   5 5 5 5 13 13 13 13 21 21 21 21 29 29 29 29 …

Нам остается лишь рассчитать свойство order для выборки элементов из четных строк:

order = --base - --num

К сожалению, математическая функция floor() в CSS только планируется, но можно воспользоваться @property, чтобы задать целочисленный тип параметра, и отнимать от значения 0.5, что и даст нам floor().

Пример с четырьмя контрольными точками адаптивности (1, 2, 3 и 4 колонки, проверять в Chrome):

@property --floor {
  syntax: '<integer>';
  initial-value: 0;
  inherits: false
}

.roadmap {
  --cols: 1;
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  gap: 10px;
}

.link {
  --floor: calc(((var(--num) - 1) / var(--cols) - .5));
  --base: calc(var(--cols) * (1 + 2 * var(--floor)) + 1);
  
  order: var(--num);

  height: 100px;
  background: yellow;
  font-size: 200%;
}

@media (min-width: 500px) and (max-width: 749px) {
  .roadmap { --cols: 2; }
  
  .link:nth-child(4n+3),
  .link:nth-child(4n+4) {
    background: orange;
    order: calc(var(--base) - var(--num));
  }
}

@media (min-width: 750px) and (max-width: 999px) {
  .roadmap { --cols: 3; }
  
  .link:nth-child(6n+4),
  .link:nth-child(6n+5),
  .link:nth-child(6n+6) {
    background: orange;
    order: calc(var(--base) - var(--num));
  }
}

@media (min-width: 1000px) {
  .roadmap { --cols: 4; }
  
  .link:nth-child(8n+5),
  .link:nth-child(8n+6),
  .link:nth-child(8n+7),
  .link:nth-child(8n+8) {
    background: orange;
    order: calc(var(--base) - var(--num));
  }
}
<div class="roadmap">
  <div class="link" style="--num: 1">1</div>
  <div class="link" style="--num: 2">2</div>
  <div class="link" style="--num: 3">3</div>
  <div class="link" style="--num: 4">4</div>
  <div class="link" style="--num: 5">5</div>
  <div class="link" style="--num: 6">6</div>
  <div class="link" style="--num: 7">7</div>
  <div class="link" style="--num: 8">8</div>
  <div class="link" style="--num: 9">9</div>
  <div class="link" style="--num: 10">10</div>
  <div class="link" style="--num: 11">11</div>
  <div class="link" style="--num: 12">12</div>
</div>

Обратите внимание, какой лаконичный и ясный CSS. Совсем несложно масштабировать для любого числа колонок.

Теперь о грустном. Оказалось, поддержка @property оставляет желать лучшего, Safari и Firefox не проглотят, а это существенно. Поэтому оставим этот вариант на будущее.


Еще одна возможность рассчитать базовое число – через номер колонки элемента. Благо, назначить его элементам несложно через :nth-child(An+B).

Новая формула для расчета:

order = --num + --cols – 2 * --col - 1

Получится немного больше CSS:

.roadmap {
  --cols: 1;

  display: grid;
  gap: 10px;
  grid-template-columns: repeat(var(--cols), 1fr);
}

.link {
  --col: 0;

  order: var(--num);
  height: 100px;
  background: yellow;
  font-size: 200%;
}

@media (min-width: 500px) and (max-width: 749px) {
  .roadmap { --cols: 2; }
  
  .link:nth-child(2n+1) { --col: 0; }
  .link:nth-child(2n+2) { --col: 1; }
  
  .link:nth-child(4n+3),
  .link:nth-child(4n+4) {
    order: calc(var(--num) + var(--cols) - 2 * var(--col) - 1);
    background: orange;
  }
}

@media (min-width: 750px) and (max-width: 999px) {
  .roadmap { --cols: 3; }
  
  .link:nth-child(3n+1) { --col: 0; }
  .link:nth-child(3n+2) { --col: 1; }
  .link:nth-child(3n+3) { --col: 2; }
  
  .link:nth-child(6n+4),
  .link:nth-child(6n+5),
  .link:nth-child(6n+6) {
    order: calc(var(--num) + var(--cols) - 2 * var(--col) - 1);
    background: orange;
  }
}

@media (min-width: 1000px) {
  .roadmap { --cols: 4; }
  
  .link:nth-child(4n+1) { --col: 0; }
  .link:nth-child(4n+2) { --col: 1; }
  .link:nth-child(4n+3) { --col: 2; }
  .link:nth-child(4n+4) { --col: 3; }
  
  .link:nth-child(8n+5),
  .link:nth-child(8n+6),
  .link:nth-child(8n+7),
  .link:nth-child(8n+8) {
    order: calc(var(--num) + var(--cols) - 2 * var(--col) - 1);
    background: orange;
  }
}
<div class="roadmap">
  <div class="link" style="--num: 1">1</div>
  <div class="link" style="--num: 2">2</div>
  <div class="link" style="--num: 3">3</div>
  <div class="link" style="--num: 4">4</div>
  <div class="link" style="--num: 5">5</div>
  <div class="link" style="--num: 6">6</div>
  <div class="link" style="--num: 7">7</div>
  <div class="link" style="--num: 8">8</div>
  <div class="link" style="--num: 9">9</div>
  <div class="link" style="--num: 10">10</div>
  <div class="link" style="--num: 11">11</div>
  <div class="link" style="--num: 12">12</div>
</div>

Итоговый стилизованный вариант:

:root {
  --color: #3f48cc;
  --line-size: 4px;
  --line-style: solid;
  --border: var(--line-size) var(--line-style) var(--color);
  --left-offset: 10px;
  
  font-family: arial;
}

.roadmap {
  --cols: 1;
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  padding-left: var(--left-offset);
  margin-top: 40px;
  counter-reset: num 0;
  color: var(--color);
}

.link {
  --row-even: 0;
  --col: 0;

  padding-bottom: 40px;
  order: var(--num);
  border-top: var(--border);
  counter-increment: num;
  position: relative;
}


@media (min-width: 500px) and (max-width: 749px) {
  .roadmap { --cols: 2; }
  
  .link:nth-child(2n+1) { --col: 0; }
  .link:nth-child(2n+2) { --col: 1; }
  
  .link:nth-child(4n+3),
  .link:nth-child(4n+4) {
    order: calc(var(--num) + var(--cols) - 2 * var(--col) - 1);
    --row-even: 1;    
  }
  
  .link:nth-child(4n+4):not(.empty, .last):after { content: ''; }
  .link:nth-child(4n+2):not(.empty, .last) {  border-right: var(--border); }  
}

@media (min-width: 750px) and (max-width: 999px) {
  .roadmap { --cols: 3; }
  
  .link:nth-child(3n+1) { --col: 0; }
  .link:nth-child(3n+2) { --col: 1; }
  .link:nth-child(3n+3) { --col: 2; }
  
  .link:nth-child(6n+4),
  .link:nth-child(6n+5),
  .link:nth-child(6n+6) {
    order: calc(var(--num) + var(--cols) - 2 * var(--col) - 1);
    --row-even: 1;
  }
  
  .link:nth-child(6n+6):not(.empty, .last):after { content: ''; }
  .link:nth-child(6n+3):not(.empty, .last) { border-right: var(--border); }  
}

@media (min-width: 1000px) {
  .roadmap { --cols: 4; }
  
  .link:nth-child(4n+1) { --col: 0; }
  .link:nth-child(4n+2) { --col: 1; }
  .link:nth-child(4n+3) { --col: 2; }
  .link:nth-child(4n+4) { --col: 3; }
  
  .link:nth-child(8n+5),
  .link:nth-child(8n+6),
  .link:nth-child(8n+7),
  .link:nth-child(8n+8) {
    order: calc(var(--num) + var(--cols) - 2 * var(--col) - 1);
    --row-even: 1;
  }
  
  .link:nth-child(8n+8):not(.empty, .last):after { content: ''; }  
  .link:nth-child(8n+4):not(.empty, .last) {  border-right: var(--border); }  
}


.link:not(.empty):before {
  content: counter(num) '.';
  position: absolute;
  box-sizing: border-box;
  height: 44px;
  top: calc(-34px - var(--line-size) / 2);
  left: 0;
  width: 20px;
  border-bottom: 20px solid var(--color);
}

.link:not(.last):after {
  position: absolute;
  width: 20px;
  border: var(--border);
  border-right: none;
  left: calc(-1 * var(--left-offset));
  top: calc(-1 * var(--line-size));
  height: 100%;
}

.link.last {
  border: none;
  margin-top: calc(var(--line-size) * (1 - var(--row-even)));
  border-top: calc(var(--line-size) * var(--row-even)) var(--line-style) var(--color);
}

.empty {
  border: none;
  padding: 0;
  margin: 0;
  height: 0;
}

.link h2 {
  font-size: 120%;
  font-weight: normal;
  margin-bottom: 0.5em;
}

.link p {
  font-size: 80%;
  margin-top: 0em;
}

@media (max-width: 499px) {
  .roadmap { margin-left: 20px; }
  
  .link {
    border: none;
    border-left: var(--border);
    padding-left: 30px;
  }
  
  .link h2 {
    margin-top: -0.5em;
  }
  
  .link:not(.empty):before {
    content: counter(num);
    text-align: right;
    top: -10px;
    left: -49px;
    padding-right: calc(var(--line-size) + 1px);
    width: 65px;

    border-bottom: none;
    height: auto;
    
    border-right: calc(var(--line-size) + 16px) solid var(--color);
  }
  
  .link.last { margin-left: var(--line-size); }
}
<div class="roadmap">
  <div class="link" style="--num: 1">
    <h2>Регистрация</h2>
  </div>
  <div class="link" style="--num: 2">
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="link" style="--num: 3">
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="link" style="--num: 4">
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="link" style="--num: 5">
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="link" style="--num: 6">
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="link" style="--num: 7">
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="link" style="--num: 8">
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="link" style="--num: 9">
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="link" style="--num: 10">
    <h2>Следующий этап</h2>
    <p>Описание</p>
  </div>
  <div class="link last" style="--num: 11">
    <h2>Доигрались</h2>
  </div>
  <div class="link empty" style="--num: 12"></div>
  <div class="link empty" style="--num: 13"></div>
  <div class="link empty" style="--num: 14"></div>
</div>

Важно. К последнему звену в цепи нужно добавить класс .last, а после, необходимо добавить элементы с классом .empty, в количестве на единицу меньшем, чем максимальное число колонок в макетах. Они не видны, но важны для правильной организации элементов в последней строке.

Надеюсь кому-то пригодится.

→ Ссылка