ФронтендHTML / CSSJavaScript

Как сделать бургер-меню на HTML, CSS и JavaScript: открытие и анимация

Делаем адаптивное бургер-меню на HTML, CSS и JavaScript: кнопка, анимация трёх линий, aria-expanded, закрытие по Escape и проверка на телефоне.

Кодик

Автор

5 мин чтения

Бургер-меню состоит из настоящей button, списка ссылок и одного класса is-open. На широком экране ссылки видны в строку, на узком они открываются по клику, закрываются по Escape и сообщают состояние через aria-expanded.

Три полоски сами по себе не являются меню. Интерфейс должен работать с клавиатуры, показывать фокус, не прятать ссылки от скринридера и не оставаться открытым после перехода на desktop. Поэтому мы сначала соберём семантику, затем анимацию.

1Размечаем

button знает id списка через aria-controls.

2Открываем

JavaScript меняет is-open и aria-expanded одновременно.

3Закрываем

Escape, клик по ссылке и переход на desktop возвращают базовое состояние.

Из какой HTML-разметки собирается меню

Кнопка и навигация должны быть соседями в header, но не обязаны лежать внутри друг друга. Для полосок используйте три span с aria-hidden, а понятное имя кнопки задайте через aria-label. На desktop кнопка display:none, а список остаётся flex.

ЭлементАтрибутЗачем
buttonaria-expandedСообщает открытое состояние
buttonaria-controlsСвязывает с nav
navidДаёт цель aria-controls
spanaria-hiddenНе засоряет имя кнопки

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

Один клик по меню
Видимое и доступное состояние меняются вместе. Escape проходит через ту же функцию

Сначала соберите минимальный сценарий. Не добавляйте оформление, хранение и дополнительные режимы одновременно. Добейтесь одного правильного прохода от входных данных до результата, а затем проверяйте каждое улучшение отдельно.

Пишем CSS и JavaScript для открытия

Анимируйте transform и opacity, а не height от 0 до auto. Браузер может плавно менять числовую max-height, но для короткого выпадающего меню проще двигать готовую панель. При prefers-reduced-motion уберите transition, чтобы не навязывать движение.

<button class="menu-button" aria-expanded="false"
        aria-controls="main-menu" aria-label="Открыть меню">
  <span aria-hidden="true"></span><span aria-hidden="true"></span>
  <span aria-hidden="true"></span>
</button>
<nav class="menu" id="main-menu">
  <a href="#home">Главная</a><a href="#projects">Проекты</a>
  <a href="#contact">Контакт</a>
</nav>
<style>
.menu-button{display:none;width:48px;height:48px;border:0;background:transparent}
.menu-button span{display:block;width:28px;height:3px;margin:5px auto;background:#151c4e;transition:.2s}
.menu{display:flex;gap:24px}.menu a{padding:12px;color:inherit;text-decoration:none}
@media(max-width:720px){
  .menu-button{display:block}.menu{position:absolute;inset:72px 16px auto;display:grid;
    padding:16px;background:white;border-radius:18px;box-shadow:0 18px 50px #14205a22;
    opacity:0;transform:translateY(-10px);pointer-events:none;transition:.2s}
  .menu.is-open{opacity:1;transform:none;pointer-events:auto}
}
@media(prefers-reduced-motion:reduce){.menu,.menu-button span{transition:none}}
</style>
<script>
const button=document.querySelector('.menu-button');
const menu=document.querySelector('#main-menu');
function setOpen(open){
  menu.classList.toggle('is-open',open);
  button.setAttribute('aria-expanded',String(open));
  button.setAttribute('aria-label',open?'Закрыть меню':'Открыть меню');
}
button.addEventListener('click',()=>setOpen(button.getAttribute('aria-expanded')!=='true'));
menu.addEventListener('click',e=>{if(e.target.closest('a'))setOpen(false)});
document.addEventListener('keydown',e=>{if(e.key==='Escape'){setOpen(false);button.focus()}});
matchMedia('(min-width:721px)').addEventListener('change',e=>{if(e.matches)setOpen(false)});
</script>
Что должно работать после первого запуска

Кнопка является button и доступна с клавиатуры. aria-expanded меняется вместе с классом. Escape закрывает меню и возвращает фокус. Переход на desktop сбрасывает мобильное состояние.

Разметка кнопки
Семантика экономит JavaScript. Сравнение показывает, какой вариант легче поддерживать и проверять.

Как aria-expanded и Escape делают меню понятным

aria-expanded не открывает меню само, но передаёт его состояние в дерево доступности. Скринридер может сказать, что кнопка свёрнута или развёрнута. aria-controls связывает кнопку с nav по id. Эти атрибуты нужно менять в той же функции, где меняется CSS-класс.

Действиеis-openaria-expanded
ЗагрузкаНетfalse
Клик по buttonДаtrue
EscapeНетfalse
Переход на desktopНетfalse

Сохраняйте в одном месте то, что может измениться. Если одно и то же значение скопировано в три функции, после первой правки они начнут расходиться. Один объект состояния и одна функция отрисовки снимают эту проблему.

Смотрите на состояние, а не только на красивый экран. Перезагрузка, повторный клик, пустой ввод и медленный ответ часто открывают ошибки, которые не видны в идеальном демонстрационном проходе.

Практика: проверяем мышь, клавиатуру и ширину

Тесируйте меню на ширине 320, 768 и 1024 пикселя. На 320 ссылки не должны выходить за края, на 768 не должен появляться промежуточный сломанный режим, а на 1024 все ссылки должны быть видны без кнопки. Пройдите элементы Tab и слушайте, как меняется состояние кнопки.

Проверка своими руками
  1. Сузьте окно до 390 пикселей и откройте меню.
  2. Перейдите к кнопке Tab и нажмите Enter.
  3. Нажмите Escape и убедитесь, что фокус вернулся.
  4. Откройте меню и кликните по ссылке.
  5. Не закрывая меню, растяните окно шире 721 пикселя.
  6. Включите reduced motion в системе и сверьте отсутствие анимации.
Готово, если выполняются все пункты

На desktop все ссылки видны без кнопки. На mobile меню открывается мышью, касанием и Enter. Escape закрывает и возвращает фокус. aria-expanded всегда совпадает с видимым состоянием.

Четыре сценария
Проверьте до полировки анимации. Пройдите все четыре сценария перед публикацией проекта.

Доводим бургер-меню до готового вида

Не превращайте бургер в ящик для всего. В мобильном меню оставьте главную навигацию, а вторичные ссылки перенесите в footer. Кнопка должна иметь площадь не меньше 44 на 44 пикселя и заметный focus-visible. Анимация полосок в крестик может быть добавлена после проверки всех состояний.

div вместо button

Такой элемент не получит клавиатуру и семантику бесплатно.

Только display none

Скринридеру нужно знать expanded и controls.

Нет Escape

Клавиатурой меню легко открыть и неудобно закрыть.

Состояние живёт после resize

На desktop класс нужно сбрасывать.

Сверьтесь с первичным источником. Поведение используемых функций и ограничения примера проверяйте по документацией MDN по aria-expanded. Документация особенно важна, когда меняются версии, политика платформы или формат ответа API.

Что получится в итоге

Готовый результат проекта: Как сделать бургер-меню на HTML, CSS и JavaScript: открытие и анимация
В итоге на узком экране появляется крупная кнопка, а ссылки выезжают в чистой карточке и остаются доступными с клавиатуры.

Короткие ответы
Почему нужен button?

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

Зачем aria-expanded?

Атрибут показывает, свёрнут или развёрнут связанный блок.

Когда сбрасывать меню?

По Escape, клику по ссылке и переходу на desktop-ширину.

Как понять, что проект готов?

Он повторяет целевой сценарий несколько раз подряд, понятно реагирует на ошибочный ввод и не теряет состояние при обычных действиях.

Можно ли копировать код целиком?

Можно как стартовую точку, но сначала замените значения из примера, затем запустите проект по шагам. Если вы не можете объяснить одну строку, её лучше разобрать до следующего шага.

Соберите мобильную шапку без ловушек

Сетку и медиа-запросы можно закрепить в курсе CSS, а события в курсе JavaScript. Общие правила точек перелома есть в гайде по адаптиву.

Если клики не срабатывают, сверьте подключение со статьё про JavaScript в HTML. В Кодике можно сузить превью до ширины телефона и сразу проверить меню.