Как сверстать табы
Эта статья основана на одном из практических заданий verstaem.online.
Вы можете читать материал как обычное руководство или сначала попробовать выполнить задание самостоятельно, а затем сравнить свое решение с подробным разбором.
Перейти к заданиюВ этом задании мы создадим интерфейс с вкладками, в котором пользователь сможет переключаться между различными разделами без перезагрузки страницы. На практике разберем, как организовать HTML-разметку для табов, оформить активное состояние элементов с помощью CSS и реализовать переключение контента на чистом JavaScript.
Создаем 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>
Теперь можно переходить к самой странице.
Весь интерфейс расположен внутри общего контейнера .content. Первым элементом идет заголовок страницы, затем список вкладок, а после него — сами блоки с контентом. Каждая вкладка представляет собой обычную кнопку внутри списка.
<h1 class="content__title">
Living Materials
</h1>
<ul class="content__list">
<li class="content__item">
<button class="content__btn content__btn--active" data-tab="architecture">Архитектура</button>
</li>
<li class="content__item">
<button class="content__btn" data-tab="interiors">Интерьеры</button>
</li>
<li class="content__item">
<button class="content__btn" data-tab="travel">Путешествия</button>
</li>
<li class="content__item">
<button class="content__btn" data-tab="interview">Интервью</button>
</li>
<li class="content__item">
<button class="content__btn" data-tab="photos">Фотографии</button>
</li>
</ul>
Обратите внимание на атрибут data-tab. Каждая кнопка получает собственное значение (architecture, interiors, travel и так далее). Оно понадобится JavaScript, чтобы понять, какой блок контента необходимо показать после нажатия.
Ниже располагаются сами вкладки. Каждая из них имеет одинаковую структуру, но отличается содержимым.
<div class="content__inner content__inner--active" data-tab-content="architecture">
<div class="content__image">
<img src="img/1.jpg" alt="Небоскребы" class="content__pic">
</div>
<div class="content__text">
<h2 class="content__subtitle">
Архитектура света
</h2>
<p class="content__subdescr">
Современные здания и работа с естественным освещением.
</p>
<div class="content__tags">
<div class="content__tag">48 страниц</div>
<div class="content__tag">12 проектов</div>
<div class="content__tag">94%</div>
</div>
<a href="#" target="_blank" class="content__link">Читать материал</a>
</div>
</div>
Здесь используется второй дата-атрибут — data-tab-content. Его значение совпадает со значением соответствующей кнопки. Именно благодаря этому JavaScript сможет связать кнопку и нужный блок.
Также обратите внимание на класс content__inner--active. Он присутствует только у первой вкладки. Именно она будет открыта сразу после загрузки страницы.
Размечаем остальные блоки по аналогии.
<div class="content__inner" data-tab-content="interiors">
<div class="content__image">
<img src="img/2.jpg" alt="Кровать с красивым бельем" class="content__pic">
</div>
<div class="content__text">
<h2 class="content__subtitle">
Тёплые интерьеры
</h2>
<p class="content__subdescr">
Домашние пространства в природной палитре.
</p>
<div class="content__tags">
<div class="content__tag">16 домов</div>
<div class="content__tag">8 дизайнеров</div>
<div class="content__tag">4 страны</div>
</div>
<a href="#" target="_blank" class="content__link">Открыть подборку</a>
</div>
</div>
<div class="content__inner" data-tab-content="travel">
<div class="content__image">
<img src="img/3.jpg" alt="Дорога в горах" class="content__pic">
</div>
<div class="content__text">
<h2 class="content__subtitle">
Маршруты Европы
</h2>
<p class="content__subdescr">
Живописные места и небольшие города.
</p>
<div class="content__tags">
<div class="content__tag">27 мест</div>
<div class="content__tag">9 городов</div>
<div class="content__tag">6 стран</div>
</div>
<a href="#" target="_blank" class="content__link">Смотреть маршрут</a>
</div>
</div>
<div class="content__inner" data-tab-content="interview">
<div class="content__image">
<img src="img/4.jpg" alt="Человек" class="content__pic">
</div>
<div class="content__text">
<h2 class="content__subtitle">
Разговоры с авторами
</h2>
<p class="content__subdescr">
Большие интервью с архитекторами и фотографами.
</p>
<div class="content__tags">
<div class="content__tag">11 гостей</div>
<div class="content__tag">6 часов</div>
<div class="content__tag">100+ фото</div>
</div>
<a href="#" target="_blank" class="content__link">Читать интервью</a>
</div>
</div>
<div class="content__inner" data-tab-content="photos">
<div class="content__image">
<img src="img/5.jpg" alt="Фотограф в горах" class="content__pic">
</div>
<div class="content__text">
<h2 class="content__subtitle">
Фотоистории
</h2>
<p class="content__subdescr">
Подборка атмосферных редакционных серий.
</p>
<div class="content__tags">
<div class="content__tag">320 фото</div>
<div class="content__tag">18 серий</div>
<div class="content__tag">5 выпусков</div>
</div>
<a href="#" target="_blank" class="content__link">Открыть галерею</a>
</div>
</div>
Добавляем стили
Подключим файл стилей через <link> и зададим несколько базовых правил. Для этого задания достаточно переопределить box-sizing, сбросить отступ у <body> и подключить шрифты, которые используются в макете.
* {
box-sizing: border-box;
}
@font-face {
font-family: "Inter";
src: url("../fonts/InterRegular.woff2");
font-display: swap;
font-weight: 400;
}
@font-face {
font-family: "PT Serif";
src: url("../fonts/PTSerifBold.woff2");
font-display: swap;
font-weight: 700;
}
body {
margin: 0;
padding: 20px;
background: #ece3d5;
font-family: "Inter", sans-serif;
}
После этого оформим основной контент.
.content {
display: flex;
flex-direction: column;
gap: 24px;
margin: 0 auto;
max-width: 1200px;
border: 1px solid #d8c8b0;
padding: 40px;
background: #f8f2e8;
}
По сути, это просто вертикальный flexbox-контейнер.
Далее заголовок и кнопки. Список кнопок делаем с помощью flexbox, кнопки стилизуем по макету. Для активности используем специальный класс content__btn--active, и помимо изменения цветов здесь пригодится свойство pointer-events: none, которое сделает невозможным клик на активный элемент, т. к. в этом уже просто нет смысла.
.content__title {
margin: 0;
font-family: "PT Serif", sans-serif;
font-weight: 700;
font-size: 52px;
line-height: 120%;
color: #2f2922;
}
.content__list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
gap: 12px;
}
.content__btn {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #cdbca5;
border-radius: 100px;
padding: 12px 24px;
font-family: inherit;
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #2f2922;
background: #efe5d6;
cursor: pointer;
transition: border-color 0.3s, color 0.3s, background 0.3s;
}
.content__btn:hover {
border-color: #3d3127;
color: #fff;
background: #3d3127;
}
.content__btn--active {
border-color: #3d3127;
color: #fff;
background: #3d3127;
pointer-events: none;
}
Далее контент вкладок. Тут все довольно просто. Все вкладки скрываем через display: none, активную показываем через display: flex. Ну, а внутри у нас два блока — изображение и блок с текстом. Тексту зададим фиксированную ширину, а изображение пусть сжимается. Кстати, именно поэтому мы и сделали для изображения обертку. Если задать самому изображению flex: 1 и max-width: 100%, оно не сожмется как надо.
.content__inner {
display: none;
gap: 28px;
}
.content__inner--active {
display: flex;
}
.content__image {
flex: 1;
}
.content__pic {
display: block;
border-radius: 12px;
max-width: 100%;
object-fit: cover;
}
.content__text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
border: 1px solid #d8c8b0;
border-radius: 12px;
padding: 24px;
flex-shrink: 0;
width: 418px;
background: #f3ebdf;
}
.content__subtitle {
margin: 0;
font-family: "PT Serif", sans-serif;
font-weight: 700;
font-size: 36px;
line-height: 120%;
color: #2f2922;
}
.content__subdescr {
margin: 0;
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #2f2922;
}
.content__tags {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.content__tag {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #d8c8b0;
border-radius: 8px;
padding: 12px;
font-weight: 400;
font-size: 16px;
line-height: 120%;
color: #000;
}
.content__link {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 100px;
padding: 12px 24px;
font-weight: 400;
font-size: 14px;
line-height: 120%;
text-decoration: none;
color: #fff;
background: #3d3127;
transition: background 0.3s;
}
.content__link:hover {
background: #584739;
}
Пишем JS
Теперь переходим к логике переключения. Сначала получим все кнопки и все вкладки.
const tabs = document.querySelectorAll('[data-tab]')
const contents = document.querySelectorAll('[data-tab-content]')
Мы нашли коллекцию элементов, поэтому ее надо перебрать через цикл.
tabs.forEach(btn => {
})
Для каждой из них добавляем обработчик события click. После нажатия первым делом получаем значение атрибута data-tab.
const tab = btn.dataset.tab
Затем снимаем активный класс со всех кнопок.
tabs.forEach(b => b.classList.remove('content__btn--active'))
И сразу добавляем его на текущую.
btn.classList.add('content__btn--active')
Остается переключить содержимое. Для этого перебираем все блоки и сравниваем значения дата-атрибутов.
contents.forEach(c => {
c.classList.toggle(
'content__inner--active',
c.dataset.tabContent === tab
)
})
Метод classList.toggle() умеет не только добавлять и удалять класс. Если вторым параметром передать логическое выражение, класс автоматически добавится при true и удалится при false. В результате после каждого нажатия на кнопку открывается только одна вкладка, а остальные автоматически скрываются.
Теперь вы умеете создавать интерфейс с вкладками, в котором контент переключается без перезагрузки страницы. Этот паттерн часто используется в карточках товаров, настройках профиля, административных панелях, лендингах и корпоративных сайтах, поэтому умение реализовывать подобный компонент пригодится практически в любом проекте.