Як створити мобільне меню з burger-кнопкою без jQuery

Для створення мобільного меню у WordPress зовсім не обов’язково підключати jQuery або сторонні JavaScript-бібліотеки.

Сучасного Vanilla JavaScript цілком достатньо, щоб реалізувати:

  • відкриття та закриття меню;
  • анімацію burger-кнопки;
  • блокування прокрутки сторінки;
  • закриття меню клавішею Escape;
  • закриття після переходу за посиланням;
  • коректні aria-* атрибути для accessibility.

У цій статті створимо просте мобільне меню для власної WordPress-теми.

1. Виводимо меню в header.php

Припустимо, що у functions.php вже зареєстрована позиція меню:

function wp_hunter_setup() {

	register_nav_menus(
		array(
			'primary' => __( 'Primary Menu', 'wp-hunter' ),
		)
	);

}
add_action( 'after_setup_theme', 'wp_hunter_setup' );

Тепер у header.php перевіримо, чи призначене меню, та додамо burger-кнопку:

<?php if ( has_nav_menu( 'primary' ) ) : ?>

	<button
		class="menu-toggle"
		type="button"
		aria-expanded="false"
		aria-controls="primary-navigation"
		aria-label="<?php esc_attr_e( 'Open menu', 'wp-hunter' ); ?>"
	>
		<span class="menu-toggle__line"></span>
		<span class="menu-toggle__line"></span>
		<span class="menu-toggle__line"></span>
	</button>

	<nav
		id="primary-navigation"
		class="primary-navigation"
		aria-label="<?php esc_attr_e( 'Primary Navigation', 'wp-hunter' ); ?>"
	>
		<?php
		wp_nav_menu(
			array(
				'theme_location' => 'primary',
				'container'      => false,
				'menu_class'     => 'primary-menu',
				'fallback_cb'    => false,
			)
		);
		?>
	</nav>

<?php endif; ?>

Тут важливими є два атрибути:

aria-expanded="false"

показує, чи відкрите меню.

А:

aria-controls="primary-navigation"

вказує, яким елементом керує кнопка.

ID:

id="primary-navigation"

відповідно встановлений для <nav>.

2. Створюємо burger-кнопку

Всередині кнопки ми використали три звичайні <span>:

<span class="menu-toggle__line"></span>
<span class="menu-toggle__line"></span>
<span class="menu-toggle__line"></span>

Саме з них за допомогою CSS створимо burger.

.menu-toggle {
	display: none;
	width: 44px;
	height: 44px;
	padding: 10px;
	border: 0;
	background: transparent;
	cursor: pointer;
}

.menu-toggle__line {
	display: block;
	width: 24px;
	height: 2px;
	margin: 5px auto;
	background: currentColor;
	transition:
		transform 0.3s ease,
		opacity 0.3s ease;
}

На desktop кнопка прихована:

display: none;

Показувати її будемо лише на мобільних пристроях.

3. Анімація burger → close

Коли меню відкрите, JavaScript змінюватиме:

aria-expanded="false"

на:

aria-expanded="true"

Це дозволяє керувати анімацією без додаткових CSS-класів:

.menu-toggle[aria-expanded="true"] .menu-toggle__line:nth-child(1) {
	transform: translateY(7px) rotate(45deg);
}

.menu-toggle[aria-expanded="true"] .menu-toggle__line:nth-child(2) {
	opacity: 0;
}

.menu-toggle[aria-expanded="true"] .menu-toggle__line:nth-child(3) {
	transform: translateY(-7px) rotate(-45deg);
}

Три лінії burger-кнопки плавно перетворяться на хрестик.

4. Стилі мобільного меню

Тепер створимо саме мобільне меню.

Наприклад, нехай воно виїжджає справа:

@media (max-width: 991px) {

	.menu-toggle {
		display: block;
		position: relative;
		z-index: 1001;
	}

	.primary-navigation {
		position: fixed;
		top: 0;
		right: 0;
		bottom: 0;
		width: min(400px, 100%);
		padding: 100px 30px 40px;
		background: #fff;

		transform: translateX(100%);
		visibility: hidden;
		transition:
			transform 0.35s ease,
			visibility 0.35s;
		z-index: 1000;
	}

	body.menu-open .primary-navigation {
		transform: translateX(0);
		visibility: visible;
	}

	.primary-menu {
		display: flex;
		flex-direction: column;
		gap: 20px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.primary-menu a {
		display: block;
		text-decoration: none;
	}

	body.menu-open {
		overflow: hidden;
	}
}

Ключовим тут є клас:

body.menu-open

Коли він з’являється на <body>, меню стає видимим.

Також:

body.menu-open {
	overflow: hidden;
}

блокує прокрутку сторінки під відкритим мобільним меню.

5. JavaScript без jQuery

Створимо файл:

/assets/js/navigation.js

і додамо:

document.addEventListener('DOMContentLoaded', () => {

	const toggle = document.querySelector('.menu-toggle');
	const navigation = document.querySelector('.primary-navigation');

	if (!toggle || !navigation) {
		return;
	}

	const openMenu = () => {
		document.body.classList.add('menu-open');

		toggle.setAttribute('aria-expanded', 'true');
		toggle.setAttribute('aria-label', 'Close menu');
	};

	const closeMenu = () => {
		document.body.classList.remove('menu-open');

		toggle.setAttribute('aria-expanded', 'false');
		toggle.setAttribute('aria-label', 'Open menu');
	};

	const toggleMenu = () => {
		const isOpen = toggle.getAttribute('aria-expanded') === 'true';

		if (isOpen) {
			closeMenu();
		} else {
			openMenu();
		}
	};

	toggle.addEventListener('click', toggleMenu);

});

Все.

Для базового мобільного меню нам не потрібні ні jQuery, ні Bootstrap, ні будь-які інші залежності.

6. Закриваємо меню клавішею Escape

Додамо можливість закрити меню клавішею Escape.

Це особливо важливо для користувачів, які працюють із сайтом за допомогою клавіатури.

document.addEventListener('keydown', (event) => {

	if (event.key === 'Escape') {
		closeMenu();
	}

});

Тепер натискання:

Esc

закриє мобільне меню.

7. Закриваємо меню після кліку на посилання

На мобільних пристроях після вибору пункту навігації меню також бажано закривати.

Додаємо:

navigation.addEventListener('click', (event) => {

	if (event.target.closest('a')) {
		closeMenu();
	}

});

Це особливо корисно, якщо меню містить anchor-посилання:

#about
#services
#contacts

Користувач натискає пункт — меню закривається, а браузер переходить до потрібної секції.

8. Закриваємо меню при кліку поза ним

Також можна закривати меню, якщо користувач натиснув у будь-якому місці за межами навігації.

document.addEventListener('click', (event) => {

	const clickedInsideMenu = navigation.contains(event.target);
	const clickedToggle = toggle.contains(event.target);

	if (!clickedInsideMenu && !clickedToggle) {
		closeMenu();
	}

});

При цьому клік на самому меню або burger-кнопці не викликатиме закриття.

9. Враховуємо зміну розміру браузера

Є ще один невеликий нюанс.

Наприклад, користувач відкрив мобільне меню, а потім збільшив ширину браузера до desktop.

На <body> все ще залишиться:

class="menu-open"

Тому краще автоматично скинути стан меню при переході на desktop.

const desktopMedia = window.matchMedia('(min-width: 992px)');

desktopMedia.addEventListener('change', (event) => {

	if (event.matches) {
		closeMenu();
	}

});

Тепер при переході вище 992px мобільне меню автоматично повернеться у початковий стан.

10. Повний JavaScript

У результаті наш navigation.js виглядатиме так:

document.addEventListener('DOMContentLoaded', () => {

	const toggle = document.querySelector('.menu-toggle');
	const navigation = document.querySelector('.primary-navigation');

	if (!toggle || !navigation) {
		return;
	}

	const openMenu = () => {
		document.body.classList.add('menu-open');

		toggle.setAttribute('aria-expanded', 'true');
		toggle.setAttribute('aria-label', 'Close menu');
	};

	const closeMenu = () => {
		document.body.classList.remove('menu-open');

		toggle.setAttribute('aria-expanded', 'false');
		toggle.setAttribute('aria-label', 'Open menu');
	};

	const toggleMenu = () => {
		const isOpen = toggle.getAttribute('aria-expanded') === 'true';

		isOpen ? closeMenu() : openMenu();
	};

	toggle.addEventListener('click', toggleMenu);

	document.addEventListener('keydown', (event) => {

		if (event.key === 'Escape') {
			closeMenu();
		}

	});

	navigation.addEventListener('click', (event) => {

		if (event.target.closest('a')) {
			closeMenu();
		}

	});

	document.addEventListener('click', (event) => {

		const clickedInsideMenu = navigation.contains(event.target);
		const clickedToggle = toggle.contains(event.target);

		if (!clickedInsideMenu && !clickedToggle) {
			closeMenu();
		}

	});

	const desktopMedia = window.matchMedia('(min-width: 992px)');

	desktopMedia.addEventListener('change', (event) => {

		if (event.matches) {
			closeMenu();
		}

	});

});

Для звичайного мобільного меню цього більш ніж достатньо.

11. Підключаємо JavaScript у WordPress

Не варто вставляти:

<script src="..."></script>

безпосередньо в header.php або footer.php.

WordPress має власний механізм підключення JavaScript-файлів — wp_enqueue_script().

Додаємо у functions.php:

function wp_hunter_scripts() {

	wp_enqueue_script(
		'wp-hunter-navigation',
		get_template_directory_uri() . '/assets/js/navigation.js',
		array(),
		wp_get_theme()->get( 'Version' ),
		array(
			'strategy'  => 'defer',
			'in_footer' => true,
		)
	);

}
add_action( 'wp_enqueue_scripts', 'wp_hunter_scripts' );

Наш скрипт не використовує jQuery, тому список залежностей порожній:

array()

Тобто WordPress не буде завантажувати jQuery заради роботи мобільного меню.

12. Повна структура

У результаті структура теми може виглядати так:

your-theme/
│
├── assets/
│   ├── css/
│   │   └── main.css
│   │
│   └── js/
│       └── navigation.js
│
├── functions.php
├── header.php
├── footer.php
├── index.php
└── style.css

У functions.php:

register_nav_menus();

реєструємо позицію меню.

У header.php:

wp_nav_menu();

виводимо навігацію та burger-кнопку.

У:

navigation.js

керуємо відкриттям та закриттям меню.

А CSS відповідає за зовнішній вигляд та анімацію.

Чому краще без jQuery?

Колись для такої задачі часто використовували:

$('.menu-toggle').on('click', function() {
	$('.menu').toggleClass('active');
});

Але сьогодні для настільки простої логіки jQuery не потрібен.

Vanilla JavaScript вже має все необхідне:

document.querySelector()
addEventListener()
classList.add()
classList.remove()
classList.toggle()
closest()
matchMedia()

У результаті ми не створюємо зайву залежність і залишаємо код теми простішим.

Не забуваємо про accessibility

Burger-кнопка повинна бути саме:

<button>

а не:

<div>

або:

<span>

Кнопка вже має правильну семантику та доступна для керування клавіатурою.

Також використовуємо:

aria-expanded="false"

щоб повідомляти стан меню.

Коли меню відкривається:

aria-expanded="true"

А через:

aria-controls="primary-navigation"

зв’язуємо кнопку безпосередньо з навігацією.

Підсумок

Для створення сучасного мобільного меню у WordPress не потрібен jQuery.

Достатньо:

wp_nav_menu()
+
CSS
+
Vanilla JavaScript

У нашому прикладі burger-кнопка:

  • відкриває та закриває меню;
  • анімується у хрестик;
  • змінює aria-expanded;
  • блокує прокрутку сторінки;
  • закривається через Escape;
  • закривається після переходу за посиланням;
  • закривається при кліку поза меню;
  • автоматично скидається при переході на desktop.

Для більшості власних WordPress-тем цього достатньо, а JavaScript залишається невеликим, зрозумілим і без сторонніх залежностей.

CSS @starting-style: анімація появи елементів без JavaScript

CSS нарешті дозволяє анімувати першу появу елемента: знайомимося з @starting-style

У CSS давно є transition, але в нього була одна неприємна особливість: перехід не запускався під час першої появи елемента на сторінці.

Наприклад, якщо ми додавали повідомлення, модальне вікно, картку товару або інший елемент у DOM через JavaScript, браузер одразу показував його у фінальному стані.

Щоб отримати плавну появу, розробникам доводилося використовувати додаткові класи, requestAnimationFrame(), setTimeout() або примусове читання layout.

Тепер це можна зробити значно простіше.

Для цього в CSS з’явилося правило:

@starting-style

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

І найважливіше — сьогодні це вже не експериментальна можливість. @starting-style входить до Baseline 2024, а глобальна підтримка браузерами становить приблизно 90,65%.


Яку проблему вирішує @starting-style

Розглянемо простий приклад.

У нас є елемент:

<div class="notice">
    Налаштування успішно збережено
</div>

І ми хочемо, щоб під час появи він плавно піднімався знизу:

.notice {
    opacity: 1;
    transform: translateY(0);

    transition:
        opacity 0.3s ease,
        transform 0.3s ease;
}

Логічно було б припустити, що браузер сам виконає перехід від якогось початкового стану.

Але проблема в тому, що коли елемент тільки з’явився, попереднього стану в нього немає.

Браузер бачить одразу:

opacity: 1;
transform: translateY(0);

Тому переходити фактично немає звідки.

Саме тут і потрібен @starting-style.


Найпростіший приклад @starting-style

Додамо початковий стан:

.notice {
    opacity: 1;
    transform: translateY(0);

    transition:
        opacity 0.3s ease,
        transform 0.3s ease;
}

@starting-style {
    .notice {
        opacity: 0;
        transform: translateY(20px);
    }
}

Тепер під час першої появи .notice браузер фактично бачить три речі:

Початковий стан
opacity: 0
translateY(20px)

        ↓ transition

Фінальний стан
opacity: 1
translateY(0)

І все це працює без JavaScript-коду, який відповідає за запуск самої анімації.

JavaScript усе ще може додати елемент у DOM, але йому більше не потрібно вручну створювати проміжний стан.


Вкладений синтаксис

Є ще один варіант запису.

@starting-style можна вкладати безпосередньо в CSS-селектор:

.notice {
    opacity: 1;
    transform: translateY(0);

    transition:
        opacity 0.3s ease,
        transform 0.3s ease;

    @starting-style {
        opacity: 0;
        transform: translateY(20px);
    }
}

Особисто мені такий варіант подобається більше.

В одному місці ми одразу бачимо:

  • фінальний стан;
  • transition;
  • початковий стан.

Для компонентного CSS це особливо зручно.


Як ми робили це раніше

До появи @starting-style типовий код виглядав приблизно так.

CSS:

.notice {
    opacity: 0;
    transform: translateY(20px);

    transition:
        opacity 0.3s ease,
        transform 0.3s ease;
}

.notice.is-visible {
    opacity: 1;
    transform: translateY(0);
}

JavaScript:

const notice = document.createElement('div');

notice.className = 'notice';
notice.textContent = 'Налаштування успішно збережено';

document.body.appendChild(notice);

requestAnimationFrame(() => {
    notice.classList.add('is-visible');
});

Навіщо тут requestAnimationFrame()?

Тому що браузеру потрібно було спочатку відрендерити:

opacity: 0;

а вже потім отримати:

opacity: 1;

Інакше він міг застосувати обидва стани в одному циклі рендерингу — і transition знову не запускався.

З @starting-style це спрощується.

JavaScript:

const notice = document.createElement('div');

notice.className = 'notice';
notice.textContent = 'Налаштування успішно збережено';

document.body.appendChild(notice);

CSS:

.notice {
    opacity: 1;
    transform: translateY(0);

    transition:
        opacity 0.3s ease,
        transform 0.3s ease;

    @starting-style {
        opacity: 0;
        transform: translateY(20px);
    }
}

Готово.

JavaScript займається логікою.

CSS — анімацією.

Саме так і повинно бути.


Де це реально корисно

@starting-style особливо добре підходить для елементів, які з’являються динамічно.

Наприклад:

AJAX-повідомлення

.ajax-message {
    opacity: 1;
    transform: translateY(0);

    transition: 0.3s ease;

    @starting-style {
        opacity: 0;
        transform: translateY(-10px);
    }
}

Це можуть бути повідомлення:

Товар додано до кошика

Форму успішно відправлено

Налаштування збережено

Сталася помилка

Toast notifications

.toast {
    opacity: 1;
    transform: translateX(0);

    transition:
        opacity 0.25s ease,
        transform 0.25s ease;

    @starting-style {
        opacity: 0;
        transform: translateX(30px);
    }
}

Під час додавання .toast у DOM він автоматично плавно з’явиться справа.


Картки, які додаються через AJAX

Наприклад, WooCommerce підвантажує товари без перезавантаження сторінки.

Можна зробити:

.product-card {
    opacity: 1;
    transform: translateY(0) scale(1);

    transition:
        opacity 0.4s ease,
        transform 0.4s ease;

    @starting-style {
        opacity: 0;
        transform: translateY(15px) scale(0.98);
    }
}

Після додавання нового елемента в DOM він сам отримає плавну появу.

Без:

element.classList.add('animate');

Без таймерів.

Без requestAnimationFrame().


Popover API + @starting-style

Ще цікавіше @starting-style виглядає разом із сучасним HTML Popover API.

HTML:

<button popovertarget="user-menu">
    Відкрити меню
</button>

<div id="user-menu" popover>
    <a href="#">Профіль</a>
    <a href="#">Налаштування</a>
    <a href="#">Вийти</a>
</div>

CSS:

[popover]:popover-open {
    opacity: 1;
    transform: translateY(0) scale(1);

    transition:
        opacity 0.2s ease,
        transform 0.2s ease;

    @starting-style {
        opacity: 0;
        transform: translateY(-8px) scale(0.96);
    }
}

Тепер popover може плавно з’являтися без JavaScript-анімацій.

MDN окремо відзначає popover та <dialog> як одні з основних сценаріїв використання @starting-style.


А що з display: none?

Ось тут стає ще цікавіше.

Історично display був однією з найбільш незручних властивостей для transition.

Наприклад:

.modal {
    display: none;
    opacity: 0;
}

.modal.active {
    display: block;
    opacity: 1;
}

Ми могли очікувати плавний opacity, але через display: none елемент фактично не існував у layout.

Сучасний CSS поступово вирішує і цю проблему.

Можна використовувати:

.modal {
    transition:
        opacity 0.3s,
        display 0.3s;

    transition-behavior: allow-discrete;
}

У поєднанні з @starting-style це відкриває можливість створювати значно складніші entry/exit transitions без великої кількості JavaScript.

Наприклад:

.modal {
    display: none;
    opacity: 0;
    transform: scale(0.95);

    transition:
        opacity 0.3s,
        transform 0.3s,
        display 0.3s allow-discrete;
}

.modal.is-open {
    display: block;
    opacity: 1;
    transform: scale(1);

    @starting-style {
        opacity: 0;
        transform: scale(0.95);
    }
}

Тут @starting-style описує стан, з якого починається поява.

Важливо розуміти, що @starting-style стосується саме CSS transitions. Для звичайних @keyframes animations він не потрібен.


@starting-style не замінює JavaScript

Тут є важливий нюанс.

Може здатися, що тепер JavaScript для UI-анімацій взагалі не потрібен.

Це не так.

@starting-style не вирішує бізнес-логіку.

Наприклад, JavaScript усе одно може бути потрібен, щоб:

document.body.appendChild(element);

або:

modal.classList.add('is-open');

або щоб отримати дані через AJAX.

Але раніше JS часто був потрібен ще й для технічного запуску анімації:

element.classList.add('initial');

requestAnimationFrame(() => {
    element.classList.add('visible');
});

Ось саме цього коду тепер у багатьох випадках можна позбутися.


Хороший приклад для WordPress

Уявімо WordPress-сайт із кастомною AJAX-формою.

Після успішної відправки ми створюємо повідомлення:

const message = document.createElement('div');

message.className = 'form-success';
message.textContent = 'Дякуємо! Повідомлення відправлено.';

form.appendChild(message);

CSS:

.form-success {
    padding: 16px 20px;
    border-radius: 10px;

    opacity: 1;
    transform: translateY(0);

    transition:
        opacity 0.35s ease,
        transform 0.35s ease;

    @starting-style {
        opacity: 0;
        transform: translateY(15px);
    }
}

JS взагалі не знає, що елемент анімований.

І це хороший поділ відповідальності:

JavaScript
↓
створює елемент

CSS
↓
вирішує, як він виглядає і з'являється

Ще один приклад — mini cart WooCommerce

У WooCommerce часто потрібно показати повідомлення після додавання товару в кошик.

Наприклад:

.woocommerce-message {
    opacity: 1;
    transform: translateY(0);

    transition:
        opacity 0.3s ease,
        transform 0.3s ease;

    @starting-style {
        opacity: 0;
        transform: translateY(-15px);
    }
}

Якщо повідомлення додається в DOM динамічно, браузер отримує стартовий стан автоматично.

Для кастомних WooCommerce-інтерфейсів це може прибрати помітну кількість допоміжного JS.


Які властивості краще анімувати

Як і з будь-якими CSS transitions, найкраще використовувати властивості, які браузер може обробляти ефективно.

Передусім:

opacity
transform

Наприклад:

@starting-style {
    .card {
        opacity: 0;
        transform: translateY(20px);
    }
}

А не намагатися без необхідності анімувати:

width
height
top
left
margin

Особливо якщо таких елементів на сторінці багато.

Для більшості появ UI-компонентів комбінації:

opacity + translate

або:

opacity + scale

цілком достатньо.


Не забуваємо про prefers-reduced-motion

Нові можливості CSS не скасовують accessibility.

Якщо користувач увімкнув у системі зменшення анімацій, це бажано враховувати.

Наприклад:

.card {
    opacity: 1;
    transform: translateY(0);

    transition:
        opacity 0.3s ease,
        transform 0.3s ease;

    @starting-style {
        opacity: 0;
        transform: translateY(20px);
    }
}

@media (prefers-reduced-motion: reduce) {
    .card {
        transition: none;
    }
}

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


Що буде у старих браузерах?

У цьому і є одна з переваг такого підходу.

Браузер, який не розуміє:

@starting-style

просто не застосує початковий стиль.

Елемент усе одно буде показаний у своєму нормальному фінальному стані.

Тобто замість:

плавної появи

користувач просто побачить:

звичайну появу

Функціональність сайту при цьому не повинна ламатися.

Це хороший приклад progressive enhancement.


Підтримка браузерами

Станом на момент написання цієї статті підтримка вже достатньо хороша для використання в сучасних проєктах.

Can I Use показує приблизно 90,65% глобального покриття.

Підтримуються сучасні версії:

Chrome
Edge
Firefox
Safari
Safari iOS
Samsung Internet

MDN позначає @starting-style як Baseline 2024 — Newly available і вказує, що функція стала доступною в актуальних основних браузерах із серпня 2024 року.

Тому для більшості нових сайтів я вже не бачу причин боятися використовувати її як progressive enhancement.


Важливий нюанс: @starting-style — це не @keyframes

Не варто плутати:

@starting-style

з:

@keyframes

Наприклад, так:

.element {
    animation: fade-in 0.3s ease;
}

@keyframes fade-in {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

і це:

.element {
    opacity: 1;
    transition: opacity 0.3s ease;

    @starting-style {
        opacity: 0;
    }
}

можуть виглядати однаково.

Але концептуально це різні речі.

animation запускає окрему CSS-анімацію.

@starting-style дає початковий стан звичайному transition, коли попереднього стилю в елемента ще не було.

І саме тому ця можливість така корисна для компонентів інтерфейсу.


Підсумок

@starting-style — невелике доповнення до CSS, але воно вирішує проблему, з якою frontend-розробники стикалися роками.

Тепер замість конструкцій на кшталт:

element.classList.add('start');

requestAnimationFrame(() => {
    element.classList.add('visible');
});

у багатьох випадках достатньо:

.element {
    opacity: 1;
    transform: translateY(0);

    transition:
        opacity 0.3s ease,
        transform 0.3s ease;

    @starting-style {
        opacity: 0;
        transform: translateY(20px);
    }
}

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

Особливо корисним @starting-style буде для:

  • модальних вікон;
  • popover;
  • toast-повідомлень;
  • AJAX-контенту;
  • форм;
  • WooCommerce-повідомлень;
  • mini cart;
  • динамічних Gutenberg-компонентів;
  • елементів, які додаються до DOM після завантаження сторінки.

Тож @starting-style уже цілком можна додавати до набору інструментів сучасного frontend-розробника.

Якщо підтримка старих браузерів для проєкту не є критичною — використовуйте.

А якщо є — використовуйте все одно як progressive enhancement: у старому браузері користувач просто не побачить анімацію, але сам інтерфейс продовжить працювати.

Реєстрація та виведення меню у WordPress

Навігаційне меню — один із базових елементів будь-якої WordPress-теми. Якщо ви розробляєте власну класичну або hybrid-тему, найзручніше зареєструвати окремі позиції меню та виводити їх у потрібних частинах шаблону.

У цій статті розберемо, як правильно зареєструвати та вивести меню у WordPress 7.1.

Реєстрація меню у WordPress

Для створення позицій меню використовується функція:

register_nav_menus();

Наприклад, створимо дві області:

  • головне меню в шапці;
  • меню у футері.

Код додаємо у functions.php теми:

function wp_hunter_setup() {

	register_nav_menus(
		array(
			'primary' => __( 'Головне меню', 'wp-hunter' ),
			'footer'  => __( 'Меню у футері', 'wp-hunter' ),
		)
	);

}
add_action( 'after_setup_theme', 'wp_hunter_setup' );

Після цього WordPress буде знати про дві позиції:

primary
footer

primary та footer — це системні ідентифікатори позицій. Їх ми будемо використовувати під час виведення меню в шаблоні.

Функція register_nav_menus() автоматично активує підтримку меню для теми, тому додатково викликати:

add_theme_support( 'menus' );

не потрібно.

Реєстрація одного меню

Якщо потрібна лише одна позиція, можна використати:

register_nav_menu();

Наприклад:

function wp_hunter_setup() {

	register_nav_menu(
		'primary',
		__( 'Головне меню', 'wp-hunter' )
	);

}
add_action( 'after_setup_theme', 'wp_hunter_setup' );

Але якщо тема має декілька меню, зручніше одразу використовувати register_nav_menus().

Виведення меню в темі

Для виведення зареєстрованого меню використовується:

wp_nav_menu();

Наприклад, у header.php:

<?php
wp_nav_menu(
	array(
		'theme_location' => 'primary',
	)
);
?>

Параметр:

'theme_location' => 'primary'

вказує WordPress, яку саме зареєстровану позицію потрібно вивести.

Налаштовуємо HTML меню

У реальному проєкті зазвичай потрібно контролювати класи та HTML-обгортку.

Наприклад:

<?php
wp_nav_menu(
	array(
		'theme_location'  => 'primary',
		'container'       => 'nav',
		'container_class' => 'header-nav',
		'menu_class'      => 'header-menu',
		'menu_id'         => 'header-menu',
		'fallback_cb'     => false,
	)
);
?>

У результаті отримаємо приблизно таку структуру:

<nav class="header-nav">
	<ul id="header-menu" class="header-menu">
		<li class="menu-item">
			<a href="/">Головна</a>
		</li>

		<li class="menu-item">
			<a href="/blog/">Блог</a>
		</li>
	</ul>
</nav>

Основні параметри тут:

'theme_location'

визначає позицію меню.

'container'

задає HTML-елемент навколо меню.

'container_class'

додає клас контейнеру.

'menu_class'

задає клас для <ul>.

'menu_id'

задає його id.

А:

'fallback_cb' => false

забороняє WordPress автоматично підставляти інше меню або список сторінок, якщо меню для цієї позиції ще не призначене.

Перевірка, чи призначене меню

Перед виведенням меню можна перевірити, чи користувач справді призначив меню для потрібної області.

Для цього використовується:

has_nav_menu();

Приклад:

<?php if ( has_nav_menu( 'primary' ) ) : ?>

	<nav class="header-nav">

		<?php
		wp_nav_menu(
			array(
				'theme_location' => 'primary',
				'container'      => false,
				'menu_class'     => 'header-menu',
				'fallback_cb'    => false,
			)
		);
		?>

	</nav>

<?php endif; ?>

Цей варіант особливо зручний, якщо ви не хочете виводити порожній <nav>.

Виведення меню у футері

З другою позицією працюємо аналогічно.

Наприклад, у footer.php:

<?php
wp_nav_menu(
	array(
		'theme_location' => 'footer',
		'container'      => 'nav',
		'menu_class'     => 'footer-menu',
		'fallback_cb'    => false,
	)
);
?>

Таким чином одне меню можна використовувати в шапці сайту, інше — у футері.

Додавання класів до посилань меню

За замовчуванням WordPress додає багато класів до <li>, але іноді потрібно додати власний клас безпосередньо до <a>.

Наприклад:

function wp_hunter_menu_link_attributes( $atts, $menu_item, $args ) {

	if ( isset( $args->theme_location ) && 'primary' === $args->theme_location ) {
		$atts['class'] = 'header-menu__link';
	}

	return $atts;
}
add_filter( 'nav_menu_link_attributes', 'wp_hunter_menu_link_attributes', 10, 3 );

Тепер посилання головного меню матимуть вигляд:

<a class="header-menu__link" href="/">
	Головна
</a>

Це значно спрощує стилізацію меню.

Клас активного пункту меню

WordPress автоматично додає спеціальні класи до активних пунктів:

current-menu-item
current-menu-parent
current-menu-ancestor

Тому поточний пункт можна оформити звичайним CSS:

.header-menu .current-menu-item > a {
	font-weight: 600;
}

Наприклад, можна додати підкреслення:

.header-menu .current-menu-item > a {
	text-decoration: underline;
	text-underline-offset: 6px;
}

Додаткова PHP-логіка для визначення активної сторінки в більшості випадків не потрібна.

Повний приклад

У functions.php:

function wp_hunter_setup() {

	register_nav_menus(
		array(
			'primary' => __( 'Головне меню', 'wp-hunter' ),
			'footer'  => __( 'Меню у футері', 'wp-hunter' ),
		)
	);

}
add_action( 'after_setup_theme', 'wp_hunter_setup' );

У header.php:

<?php if ( has_nav_menu( 'primary' ) ) : ?>

	<nav class="header-nav" aria-label="<?php esc_attr_e( 'Головна навігація', 'wp-hunter' ); ?>">

		<?php
		wp_nav_menu(
			array(
				'theme_location' => 'primary',
				'container'      => false,
				'menu_class'     => 'header-menu',
				'fallback_cb'    => false,
			)
		);
		?>

	</nav>

<?php endif; ?>

І у footer.php:

<?php
wp_nav_menu(
	array(
		'theme_location' => 'footer',
		'container'      => false,
		'menu_class'     => 'footer-menu',
		'fallback_cb'    => false,
	)
);
?>

Цього достатньо для реалізації стандартної системи меню у власній WordPress-темі.

А як щодо Block Themes?

Важливо розділяти класичні та блокові WordPress-теми.

Для classic themes WordPress рекомендує систему:

register_nav_menus()
wp_nav_menu()

У повноцінних Block Themes навігація будується переважно через блок Navigation (core/navigation) у Site Editor, тому вручну реєструвати PHP-позиції меню зазвичай не потрібно.

Якщо ж ви розробляєте власну класичну або hybrid-тему з header.php, footer.php та PHP-шаблонами, register_nav_menus() і wp_nav_menu() залишаються простим і зручним рішенням.

Підсумок

Для роботи з меню у класичній WordPress-темі достатньо двох основних функцій:

register_nav_menus();

— реєструє позиції меню;

wp_nav_menu();

— виводить меню у потрібному місці шаблону.

А зв’язок між ними відбувається через:

theme_location

Наприклад:

'primary'

реєструємо у functions.php, а потім цю ж позицію передаємо в wp_nav_menu().

Такий підхід дозволяє залишити структуру навігації керованою через WordPress, а HTML, CSS та логіку відображення — повністю контролювати на рівні власної теми.

WP-Hunter

Безпечний вхід до акаунта

Увійдіть, створіть акаунт або відновіть пароль.

З поверненням

Увійдіть, щоб продовжити.