Как сверстать табы

Эта статья основана на одном из практических заданий 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">
      Разговоры с&nbsp;авторами
    </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. В результате после каждого нажатия на кнопку открывается только одна вкладка, а остальные автоматически скрываются.

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

Решение задачи

предыдущий пост следующий пост