Создание навигации с «плавающим» фоном ссылок на чистом CSS

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Элемент .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, а также другие популярные инструменты для создания эффективных и динамичных интерфейсов. Если у вас есть интересные задачи, буду рад обсудить сотрудничество!

© Habrahabr.ru