Мега-меню шаг за шагом: HTML, CSS и JavaScript с доступностью
Как сделать мега-меню: код HTML, CSS и JavaScript для копирования, управление с клавиатуры и экранные считыватели, версия для телефона, частые ошибки и демо.
Мега-меню — это большая панель, которая раскрывается из верхнего меню и показывает сразу несколько групп ссылок, часто с короткими описаниями. Вы знаете его по интернет-магазинам и сайтам компаний с широким предложением. Хорошо сделанное, оно сокращает клиенту путь к конкретной услуге. Плохо сделанное закрывается раньше, чем к нему доедет курсор, не работает на телефоне, и им невозможно пользоваться с клавиатуры.
В этом руководстве мы построим мега-меню с нуля: HTML, CSS и около 30 строк JavaScript, без каких-либо библиотек. Код я проверил в браузере, а работающее демо можно открыть здесь. Тот же шаблон работает в моём собственном меню: кнопка «Бесплатные инструменты» вверху этой страницы.
Когда мега-меню уместно
Мега-меню пригодится, когда:
- в одной части предложения у Вас больше 7–8 страниц, например несколько видов услуг в нескольких группах,
- предложение естественно делится на категории (магазин, строительная компания, бухгалтерская контора с услугами для разных клиентов),
- Вы хотите показать рядом со ссылками краткое описание или один выделенный призыв к действию.
Если на всём сайте 5–6 страниц, достаточно обычного меню. Мега-меню с тремя ссылками выглядит пустым и лишь удлиняет код.
Как должно работать хорошее мега-меню
Прежде чем писать код, определите правила. У каждого из них есть причина:
- Открывается по клику, а с клавиатуры — клавишей Enter или пробелом. Одно лишь наведение мыши не работает на телефоне.
- Закрывается по клику в стороне, клавишей Escape и тогда, когда Вы клавишей Tab переходите дальше.
- Экранный считыватель знает, раскрыта ли панель, благодаря атрибуту
aria-expanded. - Ссылки — обычные теги
<a>в HTML, поэтому Google видит все страницы. - Между кнопкой и панелью нет зазора, в котором курсор «теряет» меню.
- На телефоне панель превращается в раскрывающийся список под кнопкой «Menu».
- У групп есть заголовки, а во всей панели не больше одного выделенного призыва к действию.

Шаг 1: HTML
Каждый раскрывающийся пункт — это кнопка и панель. Кнопка, а не ссылка, потому что она ничего не открывает по новому адресу, а лишь показывает панель. Атрибут aria-controls указывает на панель, а hidden скрывает её на старте:
<header class="top">
<div class="top__in">
<a class="logo" href="/">Pomost</a>
<button class="burger" type="button" aria-expanded="false" aria-controls="menu">Menu</button>
<nav class="nav" id="menu" aria-label="Menu główne">
<ul class="nav__list">
<li>
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="mm-oferta">Oferta</button>
<div class="mega" id="mm-oferta" hidden>
<div class="mega__in">
<div>
<p class="mega__title" id="mm-tarasy">Tarasy</p>
<ul aria-labelledby="mm-tarasy">
<li><a href="/tarasy-kompozytowe">Tarasy kompozytowe<small>Bez malowania, na lata</small></a></li>
<li><a href="/tarasy-drewniane">Tarasy drewniane<small>Modrzew, sosna, egzotyki</small></a></li>
</ul>
</div>
<!-- следующие столбцы: Сады, Аксессуары -->
<div class="mega__promo">
<b>Bezpłatny pomiar</b>
<p>Przyjedziemy, zmierzymy i w 2 dni prześlemy wycenę.</p>
<a href="/kontakt">Umów pomiar</a>
</div>
</div>
</div>
</li>
<li><a href="/realizacje">Realizacje</a></li>
<li><a href="/kontakt">Kontakt</a></li>
</ul>
</nav>
</div>
</header>
Заголовок группы — обычный абзац, а список связан с ним через aria-labelledby. Экранный считыватель тогда объявляет «Tarasy, список, 3 пункта», а в структуре заголовков страницы не появляется десяток лишних H2.
Часто встречаю в таких меню role="menu" и role="menuitem". Это ошибка: эти роли предназначены для меню приложений (как «Файл, Правка» в редакторе) и обещают управление стрелками, которого у обычного сайта нет. Для навигации руководство WAI-ARIA Authoring Practices рекомендует как раз шаблон «disclosure», то есть кнопку, которая показывает и скрывает содержимое.
Шаг 2: CSS
Важнее всего три вещи: панель примыкает к шапке без зазора, вид кнопки зависит от aria-expanded, а фокус с клавиатуры хорошо виден:
.top { position: relative; background: #fff; }
.nav__list { display: flex; gap: 4px; margin: 0; padding: 0; list-style: none; }
.nav__toggle { display: inline-flex; align-items: center; gap: 8px; padding: 10px 14px;
border: 0; border-radius: 10px; background: none; font: inherit; cursor: pointer; }
/* стрелка поворачивается, когда панель открыта: состояние читается прямо из aria-expanded */
.nav__toggle::after { content: ""; width: 7px; height: 7px; border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor; transform: translateY(-2px) rotate(45deg); transition: transform .2s; }
.nav__toggle[aria-expanded="true"]::after { transform: translateY(1px) rotate(-135deg); }
/* панель на всю ширину, начинается точно под шапкой (top: 100%, без отступа) */
.mega { position: absolute; left: 0; right: 0; top: 100%; z-index: 10; background: #fff;
box-shadow: 0 24px 40px -24px rgba(17, 21, 28, .25); }
.mega__in { max-width: 1120px; margin: 0 auto; padding: 28px 20px 32px;
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(0, 1.2fr); gap: 32px; }
.mega ul { margin: 0; padding: 0; list-style: none; }
.mega ul a { display: block; padding: 8px 10px; border-radius: 10px; text-decoration: none; }
.mega ul small { display: block; color: #5b6472; font-size: 13.5px; }
:focus-visible { outline: 3px solid #2f5bff; outline-offset: 2px; }
/* короткое появление панели, но не для тех, кто отключил анимации в системе */
@media (prefers-reduced-motion: no-preference) {
.mega:not([hidden]) { animation: wjazd .18s ease-out; }
}
@keyframes wjazd { from { opacity: 0; transform: translateY(-6px); } }
Обратите внимание на minmax(0, 1fr) в сетке. Без этого длинное слово в столбце может раздвинуть панель шире экрана.
Шаг 3: JavaScript
Скрипт делает четыре вещи: открывает и закрывает панель по клику, закрывает другие панели, обрабатывает Escape и закрывает меню, когда Вы кликаете в стороне или клавишей Tab уходите за пределы меню:
const nav = document.querySelector('.nav');
const toggles = [...nav.querySelectorAll('.nav__toggle')];
function setOpen(btn, open) {
btn.setAttribute('aria-expanded', String(open));
document.getElementById(btn.getAttribute('aria-controls')).hidden = !open;
}
function closeAll(except) {
toggles.forEach((b) => { if (b !== except) setOpen(b, false); });
}
toggles.forEach((btn) => btn.addEventListener('click', () => {
const open = btn.getAttribute('aria-expanded') !== 'true';
closeAll(btn);
setOpen(btn, open);
}));
// Escape закрывает открытую панель и возвращает фокус на её кнопку
nav.addEventListener('keydown', (e) => {
const btn = toggles.find((b) => b.getAttribute('aria-expanded') === 'true');
if (e.key === 'Escape' && btn) { setOpen(btn, false); btn.focus(); }
});
// клик вне меню и переход фокуса на элемент вне меню закрывают панель
document.addEventListener('click', (e) => { if (!nav.contains(e.target)) closeAll(); });
document.addEventListener('focusin', (e) => { if (!nav.contains(e.target)) closeAll(); });
Состояние меню записано в одном месте: в атрибутах aria-expanded и hidden. CSS читает его, чтобы поворачивать стрелку, экранный считыватель — чтобы объявлять «раскрыто» и «свёрнуто», а скрипт — чтобы решать, что закрыть. Нет отдельных классов вроде is-open, которые могли бы разойтись с тем, что слышит незрячий человек.
Любопытное наблюдение из тестов: первая версия закрывала меню по событию focusout. С клавиатуры это работало, но панель исчезала при клике по её фону, потому что браузер тогда снимает фокус с кнопки. Событие focusin на всём документе срабатывает, только когда фокус действительно попадает на другой элемент, поэтому такой проблемы нет.
Шаг 4: версия для телефона
На телефоне нет места для четырёх столбцов. Тот же HTML мы превращаем в раскрывающийся список: навигация прячется за кнопкой «Menu», а панели выстраиваются одна под другой:
.burger { display: none; }
@media (max-width: 860px) {
.burger { display: inline-flex; margin-left: auto; padding: 10px 14px;
border: 1px solid #e3e6eb; border-radius: 10px; background: none; font: inherit; }
.nav { display: none; position: absolute; left: 0; right: 0; top: 100%; z-index: 10;
max-height: calc(100dvh - 68px); overflow-y: auto; background: #fff; }
.nav.is-open { display: block; }
.nav__list { flex-direction: column; padding: 8px 12px 16px; }
.nav__toggle { width: 100%; justify-content: space-between; padding: 14px 12px; }
.mega { position: static; box-shadow: none; }
.mega__in { grid-template-columns: minmax(0, 1fr); gap: 20px; padding: 8px 12px 16px; }
}
const burger = document.querySelector('.burger');
burger.addEventListener('click', () => {
const open = burger.getAttribute('aria-expanded') !== 'true';
burger.setAttribute('aria-expanded', String(open));
nav.classList.toggle('is-open', open);
});
max-height с единицей dvh и overflow-y: auto позволяют прокручивать длинное меню на маленьком экране, а адресная строка браузера на телефоне его не закрывает.

Как проверить меню за 5 минут
Перед публикацией проверьте меню так, как им будут пользоваться люди:
- Клавиатура: клавишей Tab дойдите до «Oferta» (Предложение), Enter открывает панель, следующие нажатия Tab проходят по ссылкам, Escape закрывает панель и возвращает на кнопку. Tab за последней ссылкой закрывает панель.
- Мышь: проведите курсор от кнопки к панели по диагонали и кликните по пустому месту панели. Панель должна остаться открытой.
- Экранный считыватель: в NVDA (Windows) или VoiceOver (Mac, iPhone) кнопка должна звучать как «Oferta, кнопка, свёрнуто», а после открытия — «раскрыто».
- Телефон и увеличение: при ширине 320 px и увеличении 200% ничего не должно выходить за пределы экрана или вынуждать прокручивать страницу в сторону.
- Lighthouse в Chrome (вкладка «Доступность») выявит отсутствующие названия кнопок и слишком слабый контраст.
Общее состояние сайта, включая доступность, SEO и скорость, можно проверить за 30 секунд в моём инструменте Проверить сайт.
Самые частые ошибки в мега-меню
- Меню только при наведении. На телефоне его не открыть, а на компьютере оно открывается случайно.
- Зазор между кнопкой и панелью. Достаточно
margin-top: 8px, чтобы по дороге к панели курсор выпал из меню и всё закрылось. <a href="#">вместо кнопки. Страница прыгает наверх, а экранный считыватель объявляет ссылку, которая никуда не ведёт.role="menu"без управления стрелками. Обещание, которое страница не выполняет, хуже, чем отсутствие ARIA.- Ссылки, подгружаемые скриптом только после наведения. Google может их не увидеть.
- Слишком много ссылок. Панель со 120 пунктами — это каталог, а не навигация. Клиент ищет 2–3 секунды, и если не находит, возвращается в Google.
- Панель шире экрана на телефоне. Классический симптом: страницу можно сдвинуть вбок на несколько пикселей.
А что с WordPress?
В WordPress мега-меню обычно обеспечивает тема или плагин. Прежде чем оставить его, пройдите тот же список проверок: многие плагины открывают меню только при наведении, не устанавливают aria-expanded и добавляют на каждую страницу несколько десятков килобайт скриптов. Иногда проще и быстрее дописать собственное меню на основе кода из этого руководства.
Итог
Хорошее мега-меню — это кнопка с aria-expanded, панель с hidden, обычные ссылки в HTML и три способа закрытия: клик в стороне, Escape и Tab. На телефоне тот же код работает как раскрывающийся список. Полный код из этой статьи есть в демо: откройте его, проверьте мышью и клавиатурой, а затем посмотрите исходный код страницы.
Если у Вашего сайта широкое предложение, а меню не справляется, напишите мне. Я перестрою навигацию в Вашем нынешнем сайте или спроектирую новый сайт компании. Я отвечаю в течение 24 часов, а новые клиенты получают скидку 20% на первый заказ.
Частые вопросы
Нет, если ссылки находятся в обычном HTML-коде страницы, а не подгружаются только при наведении мыши. Тогда Google видит все страницы. Но не переусердствуйте с количеством ссылок: десяток-другой хорошо названных пунктов помогает, а несколько сотен размывают сигнал о том, какие страницы самые важные.
Лучше по клику. Наведение не работает на сенсорных экранах, а на компьютере меню открывается случайно, когда курсор лишь проезжает по шапке. Если Вам важно наведение, добавьте задержку около 200 мс и оставьте клик основным способом открытия.
Роль menu предназначена для меню приложений, таких как «Файл, Правка» в текстовом редакторе, и требует управления стрелками. Для навигации по сайту руководства WAI-ARIA рекомендуют более простой шаблон: кнопку с aria-expanded, которая показывает и скрывает список обычных ссылок.
Хорошее правило — 3–4 столбца и 3–8 ссылок в каждом. Если нужно больше, это знак, что стоит упорядочить предложение или добавить страницу категории, на которую ведёт меню.
Связанные услуги
Есть похожий проект?
Опишите, что Вам нужно, и в течение 24 часов я пришлю оценку. Новые клиенты получают скидку 20% на первый заказ.


