Анімований індикатор активного пункту меню CSS

13

Від автора: у цій статті щодо створення індикатора для меню на чистому CSS я розповім про незвичайні способи застосування суміжних селекторів і псевдо-класів. Ви можете подумати, що тут не обійшлося без JavaScript. Насправді тут немає ніякого JS, а просто по-розумному використані можливості CSS. З іншого боку, простіше було б зробити це з допомогою JS.

Нижче представлений результат:

Процес створення розіб’ємо на три етапи:

Основна структура та стилі

Створення індикатора

Змушуємо індикатор рухатися

Також ми скористаємося усіма перевагами SCSS, а це створення змінних і функцій, які значно полегшують обслуговування довгого коду.

Крок 1: Основна структура та стилі

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

  • Home
  • About
  • Writing
  • Clients
  • Contact

Не так і дивно. У нас є ul з класом PrimaryNav. Даний тег служить контейнером для елементів, кожен елемент з класом Nav-item.

Задаємо змінні

Одна з ключових особливостей меню це максимальна ширина, займана елементами меню. Елементи меню розтягуються на всю ширину контейнера. У нашому випадку ми задамо змінну $menu-items, яку будемо використовувати для розрахунків ширини кожного елемента .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;
// nav ніколи не перевищить цю ширину і розраховане нами значення у відсотках
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, в ньому містяться основні стилі меню. Створимо клас .width-indicator – це буде наш індикатор:

Ось тут ми скористаємося CSS, хоча зазвичай в голову прийшов би JavaScript. Ми прекрасно знаємо, що додавання нового класу при наведенні на елемент покажчика миші це завдання JavaScript. Однак давайте розберемо, як те ж саме можна зробити на чистому CSS.

Основна складність в отриманні кожного пункту меню для подальшої комунікації між ними. У маркірованому списку перший елемент (:first-child) може взаємодіяти з другим через суміжний селектор + або ~. Але вдруге вже не може спілкуватися з першим (CSS не можна ходити по DOM в зворотному напрямку).

Виходить, що краще всього працювати з псевдо-класом :last-child. Цей псевдо-клас працює з усіма станами сусідніх елементів, як :hover, так і :active. Тому це ідеальний кандидат для встановлення індикатора. Індикатор створюється за допомогою :before :after у псевдо-клас :last-child. Елемент :before це CSS трикутник, центрований від’ємним значенням margin’а.

// Індикатор наведення миші
.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: Змушуємо індикатор рухатися

Після створення індикатора, його необхідно змусити рухатися за курсором миші. Тут нам допоможемо могутній селектор ~. З його допомогою ми будемо порівнювати елементи з першим і останнім.

На даний момент списку ul задано позиціонування position: relative за замовчуванням. Це означає, що наш індикатор встановлений на перший пункт меню. Пересувати індикатор можна, змінюючи значення left. Так як всі пункти однакової ширини, то один зсув для псевдо-класів :before :after :last-child повинен бути дорівнює ширині .Nav-item. Пам’ятаєте нашу змінну $widths? Її можна використовувати для атрибута left. Ось так це буде виглядати в vanilla 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:

// Змінний пунктів меню
// Кількість пунктів меню плюс 1 для зсуву
$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; /* зрушуємо трикутник на активний пункт меню. */
}
} // кінець циклу @for

Зауважимо, що у трикутника :before додатковий зсув на полшіріни. Додамо до всього цього анімації і ще один цикл for для визначення місця розташування індикатора. Метод заснований на визначенні поточної сторінки. При наведенні миші на пункт меню, тобто :hover, індикатор буде рухатися. Але як тільки ви приберете миша, стан елемента знову стане is active. Красивий і акуратний спосіб зробити індикатор меню без JavaScript.

// Коли :last-child is active або :hover необхідно використовувати !important для перезапису значення
@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.