Dropdown меню
Зачем нужно
Dropdown (выпадающее меню) -- базовый компонент навигации. Используется в хедерах сайтов, контекстных меню, меню действий. Правильная реализация включает CSS-only вариант для простых случаев, JS-вариант с полной клавиатурной навигацией и поддержку доступности.
Где используется
- Навигационное меню сайта с подменю
- Меню действий (кнопка с выпадающим списком)
- Контекстное меню (правая кнопка мыши)
- Select-подобные кастомные компоненты
- Меню профиля пользователя
Подход 1: CSS-only Dropdown (hover)
Простейший вариант -- меню показывается при наведении. Подходит для навигации на desktop.
HTML
<nav class="navbar">
<ul class="nav-list">
<li class="nav-item">
<a href="/" class="nav-link">Главная</a>
</li>
<li class="nav-item has-dropdown">
<a href="#" class="nav-link">Каталог</a>
<ul class="dropdown">
<li><a href="/phones" class="dropdown__link">Телефоны</a></li>
<li><a href="/laptops" class="dropdown__link">Ноутбуки</a></li>
<li><a href="/tablets" class="dropdown__link">Планшеты</a></li>
</ul>
</li>
<li class="nav-item">
<a href="/about" class="nav-link">О нас</a>
</li>
</ul>
</nav>
CSS
.nav-list {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 4px;
}
.nav-item {
position: relative;
}
.nav-link {
display: block;
padding: 12px 16px;
text-decoration: none;
color: #333;
border-radius: 8px;
transition: background 0.2s;
}
.nav-link:hover {
background: #f0f0f0;
}
/* Dropdown по умолчанию скрыт */
.dropdown {
position: absolute;
top: 100%;
left: 0;
min-width: 200px;
list-style: none;
padding: 8px 0;
margin: 0;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
}
/* Показать при hover на родителя */
.has-dropdown:hover .dropdown,
.has-dropdown:focus-within .dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.dropdown__link {
display: block;
padding: 8px 16px;
text-decoration: none;
color: #333;
transition: background 0.15s;
}
.dropdown__link:hover {
background: #f5f5f5;
}
:focus-withinпозволяет показывать dropdown при Tab-навигации -- важно для доступности.
Подход 2: JS Toggle Dropdown (клик)
Более надёжный вариант -- меню открывается/закрывается по клику. Работает на мобильных устройствах.
HTML
<div class="dropdown-wrapper">
<button class="dropdown-trigger" aria-expanded="false" aria-haspopup="true"
aria-controls="actionsMenu" id="menuBtn">
Действия
<svg class="dropdown-trigger__icon" width="16" height="16" viewBox="0 0 16 16">
<path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<ul class="dropdown-menu" id="actionsMenu" role="menu" aria-labelledby="menuBtn">
<li role="none">
<a href="#" role="menuitem" class="dropdown-menu__item">Редактировать</a>
</li>
<li role="none">
<a href="#" role="menuitem" class="dropdown-menu__item">Дублировать</a>
</li>
<li role="separator" class="dropdown-menu__separator"></li>
<li role="none">
<a href="#" role="menuitem" class="dropdown-menu__item dropdown-menu__item--danger">
Удалить
</a>
</li>
</ul>
</div>
CSS
.dropdown-wrapper {
position: relative;
display: inline-block;
}
.dropdown-trigger {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 8px;
background: #fff;
cursor: pointer;
font-size: 14px;
}
.dropdown-trigger__icon {
transition: transform 0.2s;
}
.dropdown-trigger[aria-expanded="true"] .dropdown-trigger__icon {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
min-width: 180px;
list-style: none;
margin: 0;
padding: 4px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
opacity: 0;
visibility: hidden;
transform: scale(0.95);
transform-origin: top left;
transition: opacity 0.15s, transform 0.15s, visibility 0.15s;
}
.dropdown-menu.open() {
opacity: 1;
visibility: visible;
transform: scale(1);
}
.dropdown-menu__item {
display: block;
padding: 8px 12px;
text-decoration: none;
color: #333;
border-radius: 6px;
font-size: 14px;
transition: background 0.1s;
}
.dropdown-menu__item:hover,
.dropdown-menu__item:focus {
background: #f5f5f5;
outline: none;
}
.dropdown-menu__item--danger {
color: #e53e3e;
}
.dropdown-menu__separator {
height: 1px;
background: #eee;
margin: 4px 0;
}
JavaScript с клавиатурной навигацией
class Dropdown {
constructor(wrapper) {
this.wrapper = wrapper;
this.trigger = wrapper.querySelector('.dropdown-trigger');
this.menu = wrapper.querySelector('.dropdown-menu');
this.items = [...this.menu.querySelectorAll('[role="menuitem"]')];
this.isOpen = false;
this.focusedIndex = -1;
this.trigger.addEventListener('click', () => this.toggle);
this.trigger.addEventListener('keydown', (e) => this.handleTriggerKey(e));
this.menu.addEventListener('keydown', (e) => this.handleMenuKey(e));
// Закрытие при клике вне dropdown
document.addEventListener('click', (e) => {
if (!this.wrapper.contains(e.target)) this.close();
});
}
toggle {
this.isOpen ? this.close() : this.open();
}
open {
this.isOpen = true;
this.menu.classList.add('open');
this.trigger.setAttribute('aria-expanded', 'true');
// Фокус на первый элемент
this.focusItem(0);
}
close {
this.isOpen = false;
this.menu.classList.remove('open');
this.trigger.setAttribute('aria-expanded', 'false');
this.focusedIndex = -1;
this.trigger.focus();
}
focusItem(index) {
if (index < 0) index = this.items.length - 1;
if (index >= this.items.length) index = 0;
this.focusedIndex = index;
this.items[index].focus();
}
handleTriggerKey(e) {
switch (e.key) {
case 'ArrowDown':
case 'Enter':
case ' ':
e.preventDefault();
this.open();
break;
case 'ArrowUp':
e.preventDefault();
this.open();
this.focusItem(this.items.length - 1);
break;
}
}
handleMenuKey(e) {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.focusItem(this.focusedIndex + 1);
break;
case 'ArrowUp':
e.preventDefault();
this.focusItem(this.focusedIndex - 1);
break;
case 'Home':
e.preventDefault();
this.focusItem(0);
break;
case 'End':
e.preventDefault();
this.focusItem(this.items.length - 1);
break;
case 'Escape':
this.close();
break;
case 'Enter':
case ' ':
e.preventDefault();
this.items[this.focusedIndex]?.click();
this.close();
break;
}
}
}
// Инициализация
document.querySelectorAll('.dropdown-wrapper').forEach((el) => new Dropdown(el));
Вложенные подменю (submenu)
<li class="has-submenu" role="none">
<a href="#" role="menuitem" aria-haspopup="true" aria-expanded="false">
Экспорт
<span class="submenu-arrow">›</span>
</a>
<ul class="submenu" role="menu">
<li role="none"><a href="#" role="menuitem">PDF</a></li>
<li role="none"><a href="#" role="menuitem">CSV</a></li>
<li role="none"><a href="#" role="menuitem">JSON</a></li>
</ul>
</li>
.has-submenu {
position: relative;
}
.submenu {
position: absolute;
left: 100%;
top: 0;
min-width: 150px;
opacity: 0;
visibility: hidden;
transition: opacity 0.15s, visibility 0.15s;
}
.has-submenu:hover > .submenu,
.has-submenu:focus-within > .submenu {
opacity: 1;
visibility: visible;
}
Accessibility-чеклист
| Атрибут | Назначение |
|---|---|
aria-expanded |
Состояние открыт/закрыт |
aria-haspopup="true" |
Кнопка открывает popup |
aria-controls |
Связывает кнопку с меню |
role="menu" |
Контейнер -- меню |
role="menuitem" |
Пункт меню |
| Arrow Up/Down | Навигация между пунктами |
| Enter/Space | Активация пункта |
| Escape | Закрытие меню |
| Home/End | Первый/последний пункт |
Частые ошибки
| Ошибка | Проблема | Решение |
|---|---|---|
| Только hover без клика | Не работает на мобильных | Добавь JS toggle по клику |
| Нет клавиатурной навигации | Недоступно без мыши | Arrow keys + Enter + Escape |
| Меню не закрывается при клике вне | Висит поверх контента | document.addEventListener('click', ...) |
Нет aria-expanded |
Скринридер не знает состояние | Переключай атрибут при открытии/закрытии |
| Submenu вылезает за viewport | Не помещается справа | Проверяй getBoundingClientRect |
Связанные темы
- Popup модальное окно -- блокирующий диалог
- Tooltip -- неблокирующая подсказка
- Валидация формы в реальном времени -- формы в dropdown
- Drag and Drop -- перетаскивание из dropdown