Создаем анимированный индикатор активного элемента меню с помощью CSS

В этой статье я расскажу, как создать меню с индикатором активного элемента на чистом CSS.

Вот то, что мы будем создавать:

Просмотреть

Мы разобьем весь процесс на три этапа:

  • Базовая структура и стили;
  • Создание индикатора;
  • Перемещение индикатора.

В данном примере мы также будем использовать SCSS, чтобы воспользоваться всеми преимуществами, которые дают нам переменные и функции Sass, в долгосрочной перспективе это позволяет намного проще поддерживать такой код.

Шаг 1: Базовая структура и стили

Во-первых, давайте зададим HTML-разметку для меню с использованием маркированного списка. Мы также можем разметить имена базового класса:

<ul class="PrimaryNav">
  <li class="Nav-item">Home</li>
  <li class="Nav-item">About</li>
  <li class="Nav-item is-active">Writing</li>
  <li class="Nav-item">Clients</li>
  <li class="Nav-item">Contact</li>
</ul>

У нас есть элемент <ul> с именем класса PrimaryNav, который работает как контейнер для элементов списка внутри него, каждый с классом Nav-item.

Определение переменных

Одной из ключевых особенностей этого меню навигации является максимальная ширина. В зависимости от количества пунктов оно растягивается на всю ширину контейнера. Поэтому нам нужно задать в коде SCSS переменную $menu-items, которая затем будет использоваться для расчета значения $width каждого элемента .Nav-item.

Мы также добавили переменную $indicator-color, чтобы определить цвет, используемый для индикации активного пункта меню:

// Переменные элементов меню
// Количество элементов в меню
$menu-items: 5;
// Мы умножаем их на 1%, чтобы получить корректное значение в %
$width: (100/$menu-items) * 1%;
// Цвета
$background-color: #121212;
$indicator-color: #e82d00;

Стили элементов

Теперь мы можем задать основные стили для элементов меню:

// Родительский контейнер
.PrimaryNav {
  // Удаляем разделители по умолчанию
  list-style: none;
  // Центрируем все по горизонтали!
  margin: 50px auto;
  // Ширина меню не будет никогда превышать это значение, и с ним связаны вычисления ширины элементов в процентах
  max-width: 720px;
  padding: 0;
  width: 100%;
}

// Элементы меню
.Nav-item {
  background: #fff;
  display: block;
  float: left;
  margin: 0;
  padding: 0;
  text-align: center;
  // Текущие вычисления ширины для 5 элементов дают нам 20%
  width: $width;

  // Первый элемент меню
  &:first-child {
    border-radius: 3px 0 0 3px;
  }

  // Последний элемент меню
  &:last-child {
    border-radius: 0 3px 3px 0;
  }

  // Если элемент меню является активным, мы устанавливаем ему тот же цвет, что и для индикатора
  &.is-active a {
    color: $indicator-color;
  }

  a {
    color: $background-color;
    display: block;
    padding-top: 20px;
    padding-bottom: 20px;
    text-decoration: none;

    &:hover {
      color: $indicator-color;
    }
  }
}

Просмотреть

Шаг 2: Создание индикатора

Мы зададим для индикатора разметку способом, который предполагает использование нескольких классов. То же самое можно сделать, используя только класс .PrimaryNav, но дополнительные имена класса обеспечат большую гибкость, которая пригодится нам позже.

У нас уже есть класс .PrimaryNav, который содержит основные стили меню навигации. Теперь давайте создадим для индикатора класс .with-indicator:

<ul class="PrimaryNav with-indicator">
</ul>

Здесь мы будем использовать CSS, хотя обычно для этого используется JavaScript. Но давайте посмотрим, как это можно сделать только с помощью CSS.

Хитрость заключается в том, чтобы пункты меню взаимодействовали друг с другом. В маркированном списке первый пункт списка (:first-child) может взаимодействовать со вторым дочерним элементом либо с помощью селектора следующего элемента +, либо через ~, но второй дочерний элемент списка не может взаимодействовать с первым (не может идти в обратном направлении в DOM, наподобие нашего).

Просмотреть

Последний элемент списка :last-child отслеживает состояние всех элементов списка, расположенных на одном уровне с ним. Последний дочерний элемент может отслеживать и состояние :hover, и состояние :active для всех элементов того же уровня. Это делает его идеальным кандидатом на то, чтобы установить на нем индикатор.

Мы создаем индикатор, используя элементы :before и :after для последнего дочернего элемента. Элемент :before будет использовать Треугольник CSS и отрицательный отступ для центрирования:

// Индикатор элемента, на который наведен курсор
.with-indicator {
  // Меню "привязано" к абсолютному значению позиции псевдоэлемента last-child.
  position: relative;
.Nav-item:last-child {
  &:before, &:after {
    content: '';
    display: block;
    position: absolute;
  }

  // Треугольник CSS
  &:before {
    width: 0;
    height: 0;
    border: 6px solid transparent;
    border-top-color: $color-indicator;
    top: 0;
    left: 12.5%;
    // Исправляем смещение - оно может изменяться
    margin-left: -3px;
  }
  // Блок, который располагается позади текста
  &:after {
    width: $width;
    background: $indicator-color;
    top: -6px;
    bottom: -6px;
    left: 0;
    z-index: -1;
  }
}
}

Шаг 3: Перемещение индикатора

Теперь, когда мы задали индикатор, нам нужно сделать так, чтобы он перемещался к пункту меню, на который наведен курсор мыши. В этом нам поможет селектор ~, который мы используем для сравнения элементов между первым и последним дочерними элементами.

position:relative по умолчанию установлен на <ul>, то есть индикатор указывает на первый элемент. Мы можем перемещать индикатор от одного пункта меню к другому, изменяя для него позицию left. Все элементы меню равны по ширине.

Чтобы переместить индикатор на одну позицию, селекторы элемента :last-child для :before и :after должны иметь смещение, равное ширине .Nav-item. Помните созданную нами переменную $width? Мы используем ее для атрибута left.

Вот как это все можно задать в CSS:

.with-indicator .Nav-item:nth-child(1).is-active ~ .Nav-item:last-child:after {
  left: 0;
}
.with-indicator .Nav-item:nth-child(2).is-active ~ .Nav-item:last-child:after {
  left: 20%;
}
.with-indicator .Nav-item:nth-child(3).is-active ~ .Nav-item:last-child:after {
  left: 40%;
}
.with-indicator .Nav-item:nth-child(4).is-active:after {
  left: 60%;
}
.with-indicator .Nav-item:nth-child(5).is-active:after {
  left: 80%;
}
Давайте приведем все это в динамику с помощью Sass:
// Переменные пунктов меню
// Количество элементов в меню, плюс один для смещения
$menu-items: 5;
// Текущее количество элементов в меню
$menu-items-loop-offset: $menu-items - 1;
// Мы умножаем его на 1%, чтобы получить корректное значение в %
$width: (100/$menu-items) * 1%;
.with-indicator {
  @for $i from 1 through $menu-items-loop-offset {
    // Когда .Nav-item активен, перемещаем на него индикатор.
    .Nav-item:nth-child(#{$i}).is-active ~ .Nav-item:last-child:after {
      left:($width*$i)-$width;
    }

   .Nav-item:nth-child(#{$i}).is-active ~ .Nav-item:last-child:before {
      left:($width*$i)+($width/2)-$width; /* этот код обеспечивает перемещение треугольника на пункт меню. */
    }
  } // end @for loop

Треугольник :before имеет дополнительное смещение сверху на половину ширины от смещения left.

Теперь добавим анимацию и еще один цикл Sass for для отслеживания положения индикатора относительно того, куда сейчас наведен курсор. Когда мы наводим курсор мыши на пункт меню, он приобретает статус :hover, и индикатор перемещается. Но как только мы отводим курсор, индикатор возвращается к пункту с состоянием is-active:

// Нам нужно использовать !important, чтобы переназначить состояние, когда на элемент :last-child наведен курсор или он является активным
@for $i from 1 through $menu-items-loop-offset {
  // Когда пункт меню имеет состояние :hover, перемещаем на него индикатор.
  .Nav-item:nth-child(#{$i}):hover ~ .Nav-item:last-child:after {
    left:($width*$i)-$width !important;
  }
  .Nav-item:nth-child(#{$i}):hover ~ .Nav-item:last-child:before{
    left:($width*$i)+($width/2)-$width !important;
  }
} // конец цикла @for
// обеспечиваем, чтобы last-child взаимодействовал с самим собой
.Nav-item {
  &:last-child {
    &:hover, &.is-active {
      &:before {
        left: (100%-$width)+($width/2) !important;
      }
      &:after{
        left: 100%-$width !important;
      }
    }        
  }
}

Окончательный результат

И вот мы получили анимированное меню с индикатором активного элемента без использования JavaScript.

Просмотреть

Перевод статьи «Creating an Animated Menu Indicator with CSS Selectors» был подготовлен дружной командой проекта Сайтостроение от А до Я.