Если у вас есть аккаунт на Vercel, вы, наверное, замечали, как плавно фон ссылок в панели навигации перемещается, следуя за курсором мыши. Такого эффекта несложно добиться с помощью CSS и нескольких строчек JS. Однако, интереса ради, я решил попробовать добиться похожего эффекта на чистом CSS.

Панель навигации Vercel
Панель навигации Vercel

Ссылка на конечный результат для тех, кто спешит: https://codepen.io/simzikov/pen/zYgojrb. Остальных прошу читать далее.

Определим основные требования к компоненту:

  • При первичном наведении курсора на любую ссылку фон должен плавно появляться без горизонтальной анимации.

  • При покидании курсором панели навигации фон должен также плавно исчезать без горизонтальной анимации.

  • При перемещении курсора между ссылками фон должен плавно перемещаться от одной ссылки к другой.

  • Длина ссылок не должна быть фиксированной, фон должен адаптироваться под ссылку любого размера.

Продумываем возможные подходы для решения этой задачи:

  • Использовать для фона отдельный элемент, который будет перемещаться между ссылками.

  • Использовать индивидуальный фон для каждой ссылки и анимировать его перемещение.

Первый подход выглядит более предпочтительным, потому что нам не нужно заботиться о том, куда спрятать фон ссылок, которые в данный момент «неактивны». Однако на практике средствами одного CSS этого добиться сложно. Вот тут вы можете поиграться с одним из вариантов реализации данного подхода: https://codepen.io/simzikov/pen/XWvpEwY. Я решил остановиться на втором варианте.

Приступим к написанию кода. Сперва определимся с разметкой и добавим основные элементы:

<nav class="nav" aria-label="Navigation">
  <a class="nav-link" href="#!">
    <span class="nav-link-pill" aria-hidden="true"></span>
    <span class="nav-link-title">Overview</span>
  </a>
</nav>

Элемент .nav-link-pill будет отвечать за анимированный фон. Добавим основные стили:

body {
  font-family: sans-serif;
}

.nav {
  display: flex;
  justify-content: center;
}

.nav-link {
  position: relative;
  display: inline-flex;
  padding: 0.5rem 1rem;
  text-decoration: none;
  font-size: 0.875rem;
  color: inherit;
}

.nav-link-title {
  position: relative;
}

Здесь нет ничего сложного, мы просто пытаемся визуально приблизиться к панели навигации Vercel.

Переходим к написанию стилей для анимированного фона ссылок. Анимироваться будут три свойства: opacity, transform и visibility. Свойство visibility само по себе не анимируется, но оно отлично работает в сочетании с opacity, предоставляя нам еще один способ скрыть ненужный элемент через определенный промежуток времени, указанный с помощью transition-duration. Анимация этих свойств не вызывает перерисовку документа, поэтому будет плавной.

.nav-link-pill {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.nav-link-pill::before {
  content: "";
  position: absolute;
  inset: 0;
  background-color: gainsboro;
  border-radius: 0.25rem;
  opacity: 0;
  visibility: hidden;
  transition-property: visibility, opacity, transform;
  transition-duration: 0.25s;
}

Важно отметить значения по умолчанию для трех анимированных свойств: opacity: 0, visibility: hidden и transform: translateX(0%) (последнее является значением по умолчанию для браузера, поэтому явно не указывается). Это значения, к которым анимированный фон будет возвращаться каждый раз, когда текущая «активная» ссылка меняется. Назовем это исходным состоянием.

Добавляем стили для текущей «активной» ссылки:

.nav-link:hover > .nav-link-pill::before {
  opacity: 1;
  visibility: visible;
}

При наведении курсора на ссылку анимированный фон плавно появится за ней. Далее нужно сдвинуть анимированный фон всех предыдущих и последующих ссылок, чтобы при наведении на них происходила горизонтальная анимация:

.nav-link:has(~ .nav-link:hover) > .nav-link-pill::before {
  opacity: 1;
  transform: translateX(100%);
}

.nav-link:hover ~ .nav-link > .nav-link-pill::before {
  opacity: 1;
  transform: translateX(-100%);
}

Теперь при перемещении курсора между ссылками будет казаться, что анимированный фон двигается за ним. Здесь важна роль анимации свойства visibility. Если бы мы ее не добавили, то при первичном наведении курсора на ссылку увидели бы анимацию фона всех остальных ссылок, чего нам не нужно.

Осталось решить последнюю проблему. При быстром перемещении курсора между ссылками можно увидеть незаконченную анимацию для тех ссылок, которые не являются соседними с текущей «активной» ссылкой. Решить это поможет следующий код:

.nav-link:has(~ .nav-link + .nav-link:hover) > .nav-link-pill::before {
  transition: none;
}

.nav-link:hover + .nav-link ~ .nav-link > .nav-link-pill::before {
  transition: none;
}

Этот код позволяет анимировать только соседние с «активной» ссылкой элементы, обнуляя значение transition для остальных. К сожалению, мы все еще будем видеть незаконченную анимацию фона соседних с текущей «активной» ссылкой, однако решить эту проблему средствами CSS до конца мне пока не удалось.

Ссылка на финальный вариант: https://codepen.io/simzikov/pen/zYgojrb. Буду рад вашим комментариям!

О себе

Меня зовут Евгений, я занимаюсь front-end разработкой. В своей работе использую React, Styled Components, а также другие популярные инструменты для создания эффективных и динамичных интерфейсов. Если у вас есть интересные задачи, буду рад обсудить сотрудничество!

Комментарии (10)


  1. baldr
    14.10.2024 15:26

    Ох как я хотел бы подбросить дровишек под котел для тех дизайнеров, кто приделывает все эти "анимашечки" на сайты...

    Открываешь ссылку, чтобы прочитать статью или просто посмотреть прайс на какой-нибудь продукт, и ноут разгоняет вентилятор, чтобы успеть прорисовать супер-плавный фон, на котором крутится размытое видео (которое никто не смотрит и даже не разобрать что там).. Полупрозрачные менюшечки плавно и красиво открываются с задержкой в секунду, что и начинает бесить ровно через секунду..

    Ну блин, народ! Я зашёл на сайт не по менюшкам медленно ходить. Меню - это чисто утилитарная вещь, которая должна отрабатывать быстро, не занимать пол-экрана и не мешать мне посещать этот нужный мне сайт.

    Ну ок, вы сделали это не на javascript, а на css - вероятно это будет чуть меньше жрать процессор и память (не факт). Кроме как похвастаться другому дизайнеру - эти кнопочки полезнее не стали.

    А если я открою эту страницу с телефона - я даже не увижу всей этой красоты, зато грузить лишние килобайты кода буду всё равно.