Как сверстать выпадающее меню
Эта статья основана на одном из практических заданий verstaem.online.
Вы можете читать материал как обычное руководство или сначала попробовать выполнить задание самостоятельно, а затем сравнить свое решение с подробным разбором.
Перейти к заданиюВ этом задании мы создадим выпадающее меню профиля пользователя. На практике разберем, как правильно располагать меню относительно кнопки, управлять его отображением с помощью JavaScript, закрывать меню при клике вне области и организовывать подобные интерфейсы с использованием семантической HTML-разметки.
Создаем HTML-разметку
Сначала создадим базовую HTML-разметку с помощью Emmet. Для этого достаточно ввести ! и нажать Tab. Обратите внимание на атрибут lang. Поскольку основной контент страницы написан на русском языке, здесь используем значение ru.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Выпадающее меню</title>
</head>
<body>
</body>
</html>
Основная часть страницы состоит из шапки и демонстрационного блока. Именно в шапке располагается кнопка профиля, по нажатию на которую будет открываться выпадающее меню. Сделаем лого ссылкой, меню через nav, а правую часть дивом, в котором будет кнопка и собственно наше меню.
<header class="header">
<a href="#" class="header__logo">PULSE.</a>
<nav class="header__nav">
<ul class="header__list">
<li class="header__item">
<a href="#" class="header__link">Пространство</a>
</li>
<li class="header__item">
<a href="#" class="header__link">Проекты</a>
</li>
<li class="header__item">
<a href="#" class="header__link">Команды</a>
</li>
</ul>
</nav>
<div class="header__info">
<button class="header__btn" data-dropdown-btn>
<img src="img/avatar.png" alt="Антон" class="header__avatar">
Антон
</button>
<div class="dropdown" data-dropdown>
<div class="dropdown__top">
<img src="img/avatar.png" alt="Антон Смирнов" class="dropdown__avatar">
<div class="dropdown__text">
<div class="dropdown__name">Антон Смирнов</div>
<div class="dropdown__position">Креативный директор</div>
</div>
</div>
<nav class="dropdown__nav">
<ul class="dropdown__list">
<li class="dropdown__item">
<a href="#" class="dropdown__link" data-dropdown-close>Профиль</a>
</li>
<li class="dropdown__item">
<a href="#" class="dropdown__link" data-dropdown-close>
Уведомления
<div class="dropdown__count">8</div>
</a>
</li>
<li class="dropdown__item">
<a href="#" class="dropdown__link" data-dropdown-close>
Внешний вид
</a>
</li>
<li class="dropdown__item">
<a href="#" class="dropdown__link" data-dropdown-close>
Оплата
</a>
</li>
</ul>
</nav>
<button class="dropdown__logout" data-dropdown-close>Выйти</button>
</div>
</div>
</header>
Обратите внимание на элемент .header__info. Именно он станет контейнером сразу для двух элементов:
- кнопки открытия меню;
- самого выпадающего списка.
Такое расположение позволит без проблем позиционировать меню относительно кнопки. И кстати, у кнопки мы добавляем дата-атрибут data-dropdown-btn, чтобы можно было привязаться к нему через JS.
Само меню также имеет дата-атрибут, чтобы можно было обратиться к нему. В самом меню у нас и верхняя информация о пользователе, и меню навигации, и кнопка выхода из аккаунта. Этот элемент не входит в навигацию, т. к. не осуществляет никакой навигации, лишь выходить из аккаунта.
Также, все пункты меню имеют дата-атрибут data-dropdown-close, чтобы по клику на них можно было тоже закрыть меню.
Ниже идет основная часть страницы. Здесь будет секция с классом hero, который часто принято указывать у первых секций сайта. Внутри секцию мы поделим на две части — левую и правую. Справа будет набор элементов-преимуществ, а слева просто текст и кнопка.
<main>
<section class="hero">
<div class="hero__text">
<h1 class="hero__title">
Твори
без
лимитов
</h1>
<p class="hero__descr">
Идеи заслуживают смелого воплощения. Сотрудничайте со своей командой, черпайте вдохновение и выполняйте творческую работу быстрее.
</p>
<a href="#" class="hero__link">Начать проект</a>
</div>
<div class="hero__items">
<div class="hero__item">
<div class="hero__digit">
52
</div>
Активных задачи
</div>
<div class="hero__item">
<div class="hero__digit">
18
</div>
Членов команды онлайн
</div>
<div class="hero__item">
<div class="hero__digit">
92%
</div>
Еженедельных целей закрыто
</div>
</div>
</section>
</main>
Добавляем стили
Подключим файл стилей через <link> и зададим несколько базовых правил. Для этого задания достаточно переопределить box-sizing, сбросить отступ у <body> и подключить шрифт Manrope, который используется в макете.
* {
box-sizing: border-box;
}
@font-face {
font-family: "Manrope";
src: url("../fonts/ManropeExtraLight.woff2");
font-display: swap;
font-weight: 200;
}
@font-face {
font-family: "Manrope";
src: url("../fonts/ManropeSemiBold.woff2");
font-display: swap;
font-weight: 600;
}
@font-face {
font-family: "Manrope";
src: url("../fonts/ManropeBold.woff2");
font-display: swap;
font-weight: 700;
}
@font-face {
font-family: "Manrope";
src: url("../fonts/ManropeExtraBold.woff2");
font-display: swap;
font-weight: 800;
}
body {
margin: 0;
padding: 40px;
background: #ffe95a;
font-family: "Manrope", sans-serif;
}
Теперь стили контента. Это просто блок с обводкой, тенью и фоном, размещенный по центру.
.content {
border: 5px solid #111;
margin: 0 auto;
max-width: 1180px;
box-shadow: 14px 14px 0 0 #111;
background: #fff;
}
Далее переходим к шапке. Она построена с помощью flexbox, как и навигация внутри. Все, чтобы сделать элементы в одну строчку.
.header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
border-bottom: 5px solid #111;
padding: 24px 32px;
}
.header__logo {
font-weight: 800;
font-size: 30px;
line-height: 120%;
text-decoration: none;
color: #111;
}
.header__list {
display: flex;
margin: 0;
padding: 0;
list-style: none;
gap: 12px;
}
.header__link {
display: inline-flex;
align-items: center;
justify-content: center;
border: 3px solid #111;
padding: 12px 16px;
font-weight: 700;
font-size: 16px;
line-height: 120%;
text-decoration: none;
color: #111;
transition: background 0.3s, color 0.3s;
}
.header__link:hover {
color: #fff;
background: #111;
}
.header__info {
position: relative;
}
.header__btn {
display: flex;
align-items: center;
gap: 12px;
border: 3px solid #111;
padding: 8px 12px;
font-family: inherit;
font-weight: 700;
font-size: 16px;
line-height: 120%;
color: #111;
background: #00e88f;
cursor: pointer;
transition: background 0.3s;
}
.header__btn:hover {
background: #03c379;
}
.header__avatar {
display: block;
border: 3px solid #111;
border-radius: 100%;
width: 54px;
height: 54px;
object-fit: cover;
}
В кнопке мы также используем flexbox, но важно не это. Важно то, что для header__info мы задаем position: relative, ведь именно относительно этого элемента и будет располагаться наше меню.
Но перед тем как его стилизовать, давайте быстренько стилизуем hero и его содержимое. Здесь по факту все берем из макета, я лишь скажу о расположении. Самой секции мы задаем display: flex, левой части flex: 1, чтобы тянулась на все доступное пространство, а правую часть сделаем с фиксированной шириной.
.hero {
display: flex;
}
.hero__text {
flex: 1;
border-right: 5px solid #111;
padding: 60px;
background: #ff8458;
}
.hero__title {
margin: 0;
margin-bottom: 20px;
max-width: 410px;
font-weight: 700;
font-size: 82px;
line-height: 90%;
text-transform: uppercase;
color: #111;
}
.hero__descr {
margin: 0;
margin-bottom: 20px;
max-width: 615px;
font-weight: 600;
font-size: 22px;
line-height: 120%;
color: #111;
}
.hero__link {
display: inline-flex;
align-items: center;
justify-content: center;
border: 4px solid #111;
padding: 16px 28px;
font-weight: 800;
font-size: 16px;
line-height: 120%;
text-decoration: none;
color: #111;
background: #fff;
transition: background 0.3s, color 0.3s;
}
.hero__link:hover {
color: #fff;
background: #111;
}
.hero__items {
display: flex;
flex-direction: column;
gap: 16px;
padding: 28px;
width: 340px;
background: #fff;
}
.hero__item {
border: 4px solid #111;
padding: 20px;
font-weight: 200;
font-size: 16px;
line-height: 120%;
color: #111;
background: #f5f5f5;
}
.hero__digit {
margin-bottom: 4px;
font-weight: 700;
font-size: 32px;
line-height: 120%;
}
Теперь стили для меню. Делаем его с помощью position: absolute, поднимаем повыше засчет высокого значения z-index. Само меню расположено справа, поэтому делаем right: 0, а вот отступ сверху сделаем умно — calc: (100% + 8px). Это позволит сделать меню всегда с одним и тем же отступом в 8 пикселей, даже если header__info изменит свою высоту.
.dropdown {
position: absolute;
right: 0;
top: calc(100% + 8px);
z-index: 10;
border: 5px solid #111;
width: 320px;
box-shadow: 10px 10px 0 0 #111;
background: #fff;
transition: opacity 0.3s, visibility 0.3s;
opacity: 0;
visibility: hidden;
}
.dropdown--visible {
opacity: 1;
visibility: visible;
}
Здесь также важно добавить, что изначально меню мы скрываем с помощью opacity и visibility, и сразу добавляем класс для видимости меню. Внутренности меню делаются элементарно по макету, здесь я лишь добавлю, что чтобы сделать разные цвета для элементов меню, я решил использовать псевдоклассы: nth-child (). Это можно сделать и БЭМ-модификаторами, если вам так удобнее.
.dropdown__top {
display: flex;
align-items: center;
gap: 12px;
border-bottom: 5px solid #111;
padding: 20px;
background: #00e88f;
}
.dropdown__avatar {
display: block;
border: 3px solid #111;
border-radius: 100%;
width: 54px;
height: 54px;
object-fit: cover;
}
.dropdown__name {
margin-bottom: 4px;
font-weight: 700;
font-size: 16px;
line-height: 120%;
color: #111;
}
.dropdown__position {
font-weight: 200;
font-size: 16px;
line-height: 120%;
color: #111;
}
.dropdown__list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
}
.dropdown__link {
display: flex;
align-items: center;
border-bottom: 3px solid #111;
padding: 20px;
font-weight: 800;
font-size: 16px;
line-height: 120%;
text-decoration: none;
color: #111;
transition: background 0.3s, color 0.3s;
}
.dropdown__count {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: auto;
padding: 2px;
min-width: 24px;
height: 24px;
font-weight: 800;
font-size: 16px;
line-height: 120%;
color: #fff;
background: #111;
transition: background 0.3s, color 0.3s;
}
.dropdown__item:first-child .dropdown__link {
background: #fff;
}
.dropdown__item:nth-child(2) .dropdown__link {
background: #d9ff70;
}
.dropdown__item:nth-child(3) .dropdown__link {
background: #9eebff;
}
.dropdown__item:last-child .dropdown__link {
background: #ffd86b;
}
.dropdown__link:hover {
color: #fff;
background: #111 !important;
}
.dropdown__link:hover .dropdown__count {
color: #111;
background: #fff;
}
.dropdown__logout {
display: flex;
align-items: center;
border: none;
padding: 20px;
width: 100%;
font-family: inherit;
font-weight: 800;
font-size: 16px;
line-height: 120%;
color: #111;
cursor: pointer;
background: #d8bcff;
transition: background 0.3s, color 0.3s;
}
.dropdown__logout:hover {
color: #fff;
background: #111;
}
Пишем JavaScript
Теперь переходим к логике. Сначала получаем кнопку и само меню.
const btn = document.querySelector('[data-dropdown-btn]');
const dropdown = document.querySelector('[data-dropdown]');
Перед выполнением кода проверяем, существуют ли элементы.
if (btn && dropdown) {
}
Это хороший тон. Если разметка изменится или элемент будет отсутствовать, JavaScript не завершится ошибкой.
Далее открываем меню. Здесь достаточно просто добавить класс, который мы уже указали в CSS.
btn.addEventListener('click', (e) => {
e.stopPropagation();
dropdown.classList.toggle('dropdown--visible');
});
Обратите внимание на e.stopPropagation();. Он запрещает событию всплывать выше. Без него меню открывалось бы и сразу закрывалось, поскольку обработчик документа тоже получает событие клика.
Теперь нужно реализовать закрытие меню. Здесь код будет выглядеть сложнее, но на деле все просто.
document.addEventListener('click', (e) => {
if (e.target.closest('[data-dropdown-close]') || (!dropdown.contains(e.target) && e.target !== btn)) {
dropdown.classList.remove('dropdown--visible');
}
});
Мы вешаем обработчик клика на весь документ, но проверяем условием, а куда именно мы кликнули. Первое — это если мы кликнули на элемент с data-dropdown-close
e.target.closest('[data-dropdown-close]')
Второе — это еси мы кликнули куда-то вне окна.
!dropdown.contains(e.target)
Если выполняется какое-то из этих условий, мы просто удаляем класс у меню. В результате меню автоматически закрывается как после выбора любого пункта, так и после клика в любое место страницы.
Теперь вы умеете создавать выпадающие меню, которые открываются по нажатию на кнопку и автоматически закрываются при выборе пункта или клике вне области меню. Подобные компоненты используются практически во всех современных интерфейсах: личных кабинетах, административных панелях, корпоративных сервисах и веб-приложениях.