Как создать переключаемое мобильное меню на чистом HTML/CSS/JS

Важно: этот подход даёт полный контроль над разметкой и стилем. Вы можете подключать Tailwind или Bootstrap позже, но знание базовой реализации полезно для отладки и кастомизации.
Как создать переключаемое мобильное меню
Откройте папку проекта и создайте три файла: index.html, styles.css, script.js. Ниже — пример минимальной разметки, стилей и скрипта.
HTML:
Mobile Navigation Menu
CSS:
/* Этот блок для примера — подгоняйте под свой дизайн */
section{
width: 800px;
height: 600px;
margin-top: 50px;
margin-left: 250px;
border: solid black 1px;
background: #e6e3dc;
}
/* Позиционируем контейнер значка меню */
#toggle-container{
display: grid;
width: fit-content;
margin-left: 720px;
margin-top: 10px;
gap: 5px;
cursor: pointer;
}
/* С Stack из трёх полосок: задаём размер и цвет */
#one, #two, #three{
background: black;
width: 30px;
height: 3px;
}
.toggle-content{
display: none;
margin-left: 700px;
margin-top: 20px;
}
.toggle-content a{
display: block;
text-decoration: none;
color: black;
font-size: 30px;
}
.toggle-content a:hover{
color: blue;
}
/* Класс, который показывает меню */
.displayed{
display: block;
}JavaScript (минимальный, переключение при клике):
var toggler = document.getElementById("toggle-container");
var toggleContents = document.getElementById("toggle-content");
// Переключаем класс, который управляет видимостью
toggler.addEventListener("click", function(event){
// переключаем состояние видимости
toggleContents.classList.toggle("displayed");
// обновляем aria-* для доступности
var expanded = toggler.getAttribute('aria-expanded') === 'true';
toggler.setAttribute('aria-expanded', String(!expanded));
toggleContents.setAttribute('aria-hidden', String(expanded));
});
// Клавиатурная поддержка: Enter и Space
toggler.addEventListener('keydown', function(e){
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggler.click();
}
});Выше реализовано простое переключение: клик по значку меняет класс displayed у блока .toggle-content, и меню показывается или скрывается.
Важно: исходный пример в статье имел две распространённые ошибки — неконсистентное имя класса (display vs displayed) и обработчик, который закрывал меню не надёжно. Ниже — исправления и улучшения.
Закрытие меню при клике вне элемента (надежный вариант)
Если вы хотите, чтобы меню закрывалось при клике в любое место вне меню, используйте делегирование и проверку целевого элемента. Ниже — надёжный вариант:
// Закрывать меню при клике вне toggle-container
window.addEventListener('click', function(event){
var isClickInside = toggler.contains(event.target);
if (!isClickInside){
// если меню открыто — закрываем
if (toggleContents.classList.contains('displayed')){
toggleContents.classList.remove('displayed');
toggler.setAttribute('aria-expanded', 'false');
toggleContents.setAttribute('aria-hidden', 'true');
}
}
});Примечание: если у вас есть вложенные контролы в меню (кнопки, формы), проверка через .contains() корректно учитывает клики внутри.
Доступность и UX улучшения
- Добавьте role=”button”, tabindex=”0” и aria-expanded/aria-controls, как в примере HTML.
- Обеспечьте клавиатурную навигацию: Enter и пробел должны переключать меню.
- При сложных меню рассмотрите управление фокусом (focus trap) и возврат фокуса к источнику при закрытии.
Исправление распространённых ошибок
Факт: часто встречаются две ошибки:
- Проверка и удаление класса ищет “display”, тогда как JavaScript добавляет “displayed”. Это приводит к тому, что меню не закрывается корректно.
- Обработчик клика навешан на document без проверки, что клик был вне меню — это может вызывать непредсказуемое поведение.
Исправление: используйте согласованное имя класса (displayed), проверяйте event.target с помощью .contains(), и обновляйте aria-атрибуты.
Мини‑методология: быстрый чек-лист перед публикацией
- Меню работает при клике по иконке
- Меню закрывается при клике вне элемента
- Клавиатурная навигация: Enter / Space
- aria-expanded и aria-hidden выставляются корректно
- Нет конфликтов имён классов
- Стиль меню адаптирован под экран < 768px
Критерии приёмки
- При клике на иконку меню появляется список навигации.
- Повторный клик скрывает список.
- Клик вне меню скрывает меню, если оно открыто.
- Управление работает клавиатурой (Enter/Space).
- aria-expanded меняется при каждом переключении.
Когда этот подход не сработает
- На очень сложных сайтах с несколькими слоями оверлеев стоит использовать focus trap и блокировку скролла.
- Если требуются анимации на уровне производительности (много элементов одновременно), нужна оптимизация CSS-анимаций или использование requestAnimationFrame.
- Для мультиязычных интерфейсов с динамическим контентом потребуется дополнительная логика для управления фокусом и ориентацией.
Альтернативные подходы
- Использовать CSS-only решение через input[type=”checkbox”] + :checked для простых меню (без JavaScript).
- Подключать готовые компоненты из UI-библиотек (если важна скорость разработки).
- Использовать библиотеку управления фокусом (focus-trap) для сложных модальных меню.
Примеры улучшений (идеи)
- Анимация: плавное появление меню через transform и opacity.
- Тёмная/светлая тема для меню.
- Иконка гамбургера превращается в крестик при открытии (анимация полосок).
- Тесты: добавить E2E тест, проверяющий открытие/закрытие меню и поведение при клике вне.
Итог
Вы реализовали простое переключаемое мобильное меню без фреймворков. Такой подход даёт гибкость и точный контроль над доступностью и поведением. Для продакшн‑варианта добавьте обработку клавиатуры, проверку кликов вне элемента и мелкие визуальные улучшения.
Важно: всегда тестируйте на устройствах с тач‑управлением и с клавиатурной навигацией.
Краткие выводы:
- Простая реализация — три div + скрытый блок навигации.
- Управление видимостью — класс CSS, переключаемый JavaScript.
- Улучшения: aria-атрибуты, закрытие по клику вне, клавиатурная поддержка.
Похожие материалы
Сброс Sonos до заводских настроек — инструкция
Django CBV: CRUD менеджер задач
Миниатюра YouTube в Canva — быстрый гид
Как сэкономить мобильные данные на iPhone
Музыка на нескольких Google Nest — группа динамиков