Вариaнт адаптивных вкладок (tabs) на JavaScript

Практическая статья по созданию адаптивных вкладок (tabs), используя JavaScript

Существует достаточно много способов реализовать вкладки на JavaScript. Можно выделить, наверно, два основных типа реализации - 1. по индексу и 2. по data-атрибутам

В данной статье рассмотрим второй вариант для большего контроля над переключением вкладок и более гибкой HTML-структуры

Результат можно сразу посмотреть на Codepen или прямо в статье чуть ниже

Верстаем основу

Сначала сделаем структуру HTML по методологии БЭМ

Добавим data-атрибуты и назначим одинаковые значения для соответствущих кнопок и блоков контента

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Tabs</title>
    <link rel="stylesheet" href="css/bootstrap-reboot.min.css"/>
    <link rel="stylesheet" href="css/main.css"/>
  </head>
  <body>
    <section class="section">
      <div class="container">
        <div class="section__tabs tabs">
          <div class="tabs__head">
            <div class="tabs__caption" data-tab="home">Home</div>
            <div class="tabs__caption" data-tab="portfolio">Portfolio</div>
            <div class="tabs__caption" data-tab="about">About</div>
          </div>
          <div class="tabs__body">
            <div class="tabs__content" data-tab="home">Home lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia sit, repellat assumenda excepturi minima debitis atque nulla quibusdam iste eligendi voluptas, obcaecati nihil necessitatibus vel illum itaque ea molestias vero! Home lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia sit, repellat assumenda excepturi minima debitis atque nulla quibusdam iste eligendi voluptas, obcaecati nihil necessitatibus vel illum itaque ea molestias vero!</div>
            <div class="tabs__content" data-tab="portfolio">Portfolio lorem ipsum dolor sit amet, consectetur adipisicing elit. Id magni sit, enim tenetur animi eius ea, similique optio nostrum quibusdam ex, dolores dolorem. Recusandae quo molestiae modi saepe ratione numquam!</div>
            <div class="tabs__content" data-tab="about">About lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia quibusdam nemo qui numquam magnam! In sunt, ut cum nulla, accusantium mollitia et illum voluptate repudiandae! Magnam alias, iure saepe animi.</div>
          </div>
        </div>
      </div>
    </section>
    <script src="js/main.js"></script>
  </body>
</html>


Добавим CSS стили

Для адаптивности используем flexbox

Для плавного появления контента используем @keyframes и animation

Для большего контроля и более удобного переназначения цветов используем CSS-переменные

body {
  background: #121212;
}
.container {
  max-width: 1140px;
  padding: 0 30px;
  margin: 0 auto;
}
.tabs {
  overflow: hidden;
  border-radius: 15px;
}
.tabs__head {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
}
.tabs__caption {
  flex: 1;
  text-align: center;
  text-transform: uppercase;
  padding: 8px 30px;
  font-size: 18px;
  cursor: pointer;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  color: var(--color);
  opacity: 0.6;
  background: var(--primary);
  transition: 0.25s;
}
.tabs__caption:hover {
  opacity: 0.8;
}
.tabs__caption_active {
  opacity: 1;
  background: var(--primary);
}
.tabs__body {
  background: var(--primary);
  position: relative;
  top: -2px;
  transition: 0.25s;
}
.tabs__content {
  display: none;
  padding: 30px;
  color: var(--color);
  transform: scaleY(0.8);
  opacity: 0;
  transform-origin: center top;
  -webkit-animation: showContent 0.6s forwards;
  animation: showContent 0.6s forwards;
}
.tabs__content_active {
  display: block;
}
.section {
  --primary: #3fa7d6;
  --color: #fff;
  padding: 15px 0;
}
.about {
  padding: 15px 0;
  --primary: #8cff98;
  --color: #121212;
}
@-webkit-keyframes showContent {
  0% {
    transform: scaleY(0.8);
    opacity: 0;
  }
  100% {
    transform: scaleY(1);
    opacity: 1;
  }
}
@keyframes showContent {
  0% {
    transform: scaleY(0.8);
    opacity: 0;
  }
  100% {
    transform: scaleY(1);
    opacity: 1;
  }
}


Пишем логику на JavaScript

Для начала реализуем логику для вкладок, которые мы сверстали выше.

Вся логика сводится к тому, чтобы при переключении сравнивать значения data-атрибутов кнопок и блоков контента. Если значение data-атрибута у блока контента совпадает с data-атрибутом кнопки, то отображаем соответствующий блок контента

Отличие данного способа в том, что порядок расположения кнопок и порядок расположения блоков контента в HTML-структуре могут не совпадать, при этом переключение будет работать корректно, что невозможно при реализации вкладок по индексу

Код с комментариями

document.addEventListener('DOMContentLoaded', () => { // Структура страницы загружена и готова к взаимодействию

  const tabs = () => { // объявляем основную функцию для вкладок, чтобы вся логика была в одном месте
    const head = document.querySelector('.tabs__head') // ищем элемент с кнопками и записываем в константу
    const body = document.querySelector('.tabs__body') // ищем элемент с контентом и записываем в константу

    const getActiveTabName = () => { // объявляем функцию для получения названия активной вкладки
      return head.querySelector('.tabs__caption_active').dataset.tab // возвращаем значение data-tab активной кнопки
    }

    const setActiveContent = () => { // объявляем функцию для установки активного элемента контента
      if (body.querySelector('.tabs__content_active')) { // если уже есть активный элемент контента
        body.querySelector('.tabs__content_active').classList.remove('tabs__content_active') // то скрываем его
      }
      body.querySelector(`[data-tab=${getActiveTabName()}]`).classList.add('tabs__content_active') // затем ищем элемент контента, у которого значение data-tab совпадает со значением data-tab активной кнопки и отображаем его
    }

    // проверяем при загрузке страницы, есть ли активная вкладка
    if (!head.querySelector('.tabs__caption_active')) {  // если активной вкладки нет
      head.querySelector('.tabs__caption').classList.add('tabs__caption_active') // то делаем активной по-умолчанию первую вкладку
    }

    setActiveContent(getActiveTabName()) // устанавливаем активный элемент контента в соответствии с активной кнопкой при загрузке страницы

    head.addEventListener('click', e => { // при клике на .tabs__head
      const caption = e.target.closest('.tabs__caption') // узнаем, был ли клик на кнопке
      if (!caption) return // если клик был не на кнопке, то прерываем выполнение функции
      if (caption.classList.contains('tabs__caption_active')) return // если клик был на активной кнопке, то тоже прерываем выполнение функции и ничего не делаем

      if (head.querySelector('.tabs__caption_active')) { // если уже есть активная кнопка
        head.querySelector('.tabs__caption_active').classList.remove('tabs__caption_active') // то удаляем ей активный класс
      }

      caption.classList.add('tabs__caption_active') // затем добавляем активный класс кнопке, на которой был клик

      setActiveContent(getActiveTabName()) // устанавливаем активный элемент контента в соответствии с активной кнопкой
    })
  }

  tabs() // вызываем основную функцию

})

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

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



Функция c параметрами

Добавим для примера еще один блок с вкладками

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Tabs</title>
    <link rel="stylesheet" href="css/bootstrap-reboot.min.css"/>
    <link rel="stylesheet" href="css/main.css"/>
  </head>
  <body>
    <section class="section">
      <div class="container">
        <div class="section__tabs tabs">
          <div class="tabs__head">
            <div class="tabs__caption" data-tab="home">Home</div>
            <div class="tabs__caption" data-tab="portfolio">Portfolio</div>
            <div class="tabs__caption" data-tab="about">About</div>
          </div>
          <div class="tabs__body">
            <div class="tabs__content" data-tab="home">Home lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia sit, repellat assumenda excepturi minima debitis atque nulla quibusdam iste eligendi voluptas, obcaecati nihil necessitatibus vel illum itaque ea molestias vero! Home lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia sit, repellat assumenda excepturi minima debitis atque nulla quibusdam iste eligendi voluptas, obcaecati nihil necessitatibus vel illum itaque ea molestias vero!</div>
            <div class="tabs__content" data-tab="portfolio">Portfolio lorem ipsum dolor sit amet, consectetur adipisicing elit. Id magni sit, enim tenetur animi eius ea, similique optio nostrum quibusdam ex, dolores dolorem. Recusandae quo molestiae modi saepe ratione numquam!</div>
            <div class="tabs__content" data-tab="about">About lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia quibusdam nemo qui numquam magnam! In sunt, ut cum nulla, accusantium mollitia et illum voluptate repudiandae! Magnam alias, iure saepe animi.</div>
          </div>
        </div>
      </div>
    </section>
    <section class="about">
      <div class="container">
        <div class="about__tabs tabs">
          <div class="tabs__head">
            <div class="tabs__caption" data-tab="home">Home</div>
            <div class="tabs__caption" data-tab="portfolio">Portfolio</div>
            <div class="tabs__caption" data-tab="about">About</div>
            <div class="tabs__caption" data-tab="contact">Contact</div>
          </div>
          <div class="tabs__body">
            <div class="tabs__content" data-tab="home">Home lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia sit, repellat assumenda excepturi minima debitis atque nulla quibusdam iste eligendi voluptas, obcaecati nihil necessitatibus vel illum itaque ea molestias vero!</div>
            <div class="tabs__content" data-tab="portfolio">Portfolio lorem ipsum dolor sit amet, consectetur adipisicing elit. Id magni sit, enim tenetur animi eius ea, similique optio nostrum quibusdam ex, dolores dolorem. Recusandae quo molestiae modi saepe ratione numquam!</div>
            <div class="tabs__content" data-tab="about">About lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia quibusdam nemo qui numquam magnam! In sunt, ut cum nulla, accusantium mollitia et illum voluptate repudiandae! Magnam alias, iure saepe animi.</div>
            <div class="tabs__content" data-tab="contact">Contact lorem ipsum dolor sit amet, consectetur adipisicing elit. Quod, ullam ducimus et facere nisi a dicta, ea, quae minima sequi expedita facilis doloremque excepturi, earum soluta magni! Dolores, expedita, ea.</div>
          </div>
        </div>
      </div>
    </section>
    <script src="js/main.js"></script>
  </body>
</html>


Теперь при объявлении функции tabs указываем параметры, которые будет принимать функция

При вызове функции, все параметры обязательны для ввода, и порядок передаваемых параметров должен соблюдаться

JavaScript код с комментариями

document.addEventListener('DOMContentLoaded', () => { // Структура страницы загружена и готова к взаимодействию

  const tabs = (tabsSelector, tabsHeadSelector, tabsBodySelector, tabsCaptionSelector, tabsCaptionActiveClass, tabsContentActiveClass) => { // объявляем основную функцию tabs, которая будет принимать CSS классы и селекторы

    const tabs = document.querySelector(tabsSelector) // ищем на странице элемент по переданному селектору основного элемента вкладок и записываем в константу
    const head = tabs.querySelector(tabsHeadSelector) // ищем в элементе tabs элемент с кнопками по переданному селектору и записываем в константу
    const body = tabs.querySelector(tabsBodySelector) // ищем в элементе tabs элемент с контентом по переданному селектору и записываем в константу

    const getActiveTabName = () => { // функция для получения названия активной вкладки
      return head.querySelector(`.${tabsCaptionActiveClass}`).dataset.tab // возвращаем значение data-tab активной кнопки
    }

    const setActiveContent = () => { // функция для установки активного элемента контента
      if (body.querySelector(`.${tabsContentActiveClass}`)) { // если уже есть активный элемент контента
        body.querySelector(`.${tabsContentActiveClass}`).classList.remove(tabsContentActiveClass) // то скрываем его
      }
      body.querySelector(`[data-tab=${getActiveTabName()}]`).classList.add(tabsContentActiveClass) // затем ищем элемент контента, у которого значение data-tab совпадает со значением data-tab активной кнопки и отображаем его
    }

    // проверяем при загрузке страницы, есть ли активная вкладка
    if (!head.querySelector(`.${tabsCaptionActiveClass}`)) { // если активной вкладки нет
      head.querySelector(tabsCaptionSelector).classList.add(tabsCaptionActiveClass) // то делаем активной по-умолчанию первую вкладку
    }

    setActiveContent(getActiveTabName()) // устанавливаем активный элемент контента в соответствии с активной кнопкой при загрузке страницы

    head.addEventListener('click', e => { // при клике на элемент с кнопками
      const caption = e.target.closest(tabsCaptionSelector) // узнаем, был ли клик на кнопке
      if (!caption) return // если клик был не на кнопке, то прерываем выполнение функции
      if (caption.classList.contains(tabsCaptionActiveClass)) return // если клик был на активной кнопке, то тоже прерываем выполнение функции и ничего не делаем

      if (head.querySelector(`.${tabsCaptionActiveClass}`)) { // если уже есть активная кнопка
        head.querySelector(`.${tabsCaptionActiveClass}`).classList.remove(tabsCaptionActiveClass) // то удаляем ей активный класс
      }

      caption.classList.add(tabsCaptionActiveClass) // затем добавляем активный класс кнопке, на которой был клик

      setActiveContent(getActiveTabName()) // устанавливаем активный элемент контента в соответствии с активной кнопкой
    })
  }

  tabs('.section__tabs', '.tabs__head', '.tabs__body', '.tabs__caption', 'tabs__caption_active', 'tabs__content_active') // вызываем основную функцию tabs для синих вкладок .section__tabs

  tabs('.about__tabs', '.tabs__head', '.tabs__body', '.tabs__caption', 'tabs__caption_active', 'tabs__content_active') // вызываем основную функцию tabs для зелёных вкладок .about__tabs

})


Получаем следующий результат - вкладки активны

Home
Portfolio
About
Home lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia sit, repellat assumenda excepturi minima debitis atque nulla quibusdam iste eligendi voluptas, obcaecati nihil necessitatibus vel illum itaque ea molestias vero! Home lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia sit, repellat assumenda excepturi minima debitis atque nulla quibusdam iste eligendi voluptas, obcaecati nihil necessitatibus vel illum itaque ea molestias vero!
Portfolio lorem ipsum dolor sit amet, consectetur adipisicing elit. Id magni sit, enim tenetur animi eius ea, similique optio nostrum quibusdam ex, dolores dolorem. Recusandae quo molestiae modi saepe ratione numquam!
About lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia quibusdam nemo qui numquam magnam! In sunt, ut cum nulla, accusantium mollitia et illum voluptate repudiandae! Magnam alias, iure saepe animi.
Home
Portfolio
About
Contact
Home lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia sit, repellat assumenda excepturi minima debitis atque nulla quibusdam iste eligendi voluptas, obcaecati nihil necessitatibus vel illum itaque ea molestias vero!
Portfolio lorem ipsum dolor sit amet, consectetur adipisicing elit. Id magni sit, enim tenetur animi eius ea, similique optio nostrum quibusdam ex, dolores dolorem. Recusandae quo molestiae modi saepe ratione numquam!
About lorem ipsum dolor sit amet, consectetur adipisicing elit. Mollitia quibusdam nemo qui numquam magnam! In sunt, ut cum nulla, accusantium mollitia et illum voluptate repudiandae! Magnam alias, iure saepe animi.
Contact lorem ipsum dolor sit amet, consectetur adipisicing elit. Quod, ullam ducimus et facere nisi a dicta, ea, quae minima sequi expedita facilis doloremque excepturi, earum soluta magni! Dolores, expedita, ea.


Вкладки на основе JavaScript классов

Для разнообразия/сравнения, можем также написать функциональность вкладок на основе JavaScript классов

Чтобы познакомиться с JavaScript классами, внизу статьи будут ссылки на документацию

В данном примере просто посмотрим еще один способ

В качестве параметров будем передавать JavaScript объект, а также выделим некоторую функциональность в отдельные методы

Код с комментариями

document.addEventListener('DOMContentLoaded', () => { // Структура страницы загружена и готова к взаимодействию

  class Tabs { // объявляем JavaScript класс Tabs
    constructor(config) { // специальный метод constructor принимает объект со значениями CSS классов и селекторов
      this.tabs = config.tabsSelector // записываем в экземпляр JavaScript класса переданный селектор основного элемента вкладок
      this.head = config.tabsHeadSelector // записываем в экземпляр JavaScript класса переданный селектор элемента c кнопками
      this.body = config.tabsBodySelector // записываем в экземпляр JavaScript класса переданный селектор элемента c контентом
      this.caption = config.tabsCaptionSelector // записываем в экземпляр JavaScript класса переданный селектор элемента кнопки
      this.captionActiveClass = config.tabsCaptionActiveClass // записываем в экземпляр JavaScript класса переданный активный CSS класс элемента кнопки
      this.contentActiveClass = config.tabsContentActiveClass // записываем в экземпляр JavaScript класса переданный активный CSS класс элемента контента
    }

    getActiveTabName(head) { // метод для получения названия активной вкладки
      return head.querySelector(`.${this.captionActiveClass}`).dataset.tab // возвращаем значение data-tab активной кнопки
    }

    setActiveContent(head, body) { // метод для установки активного элемента контента
      if (body.querySelector(`.${this.contentActiveClass}`)) { // если уже есть активный элемент контента
        body.querySelector(`.${this.contentActiveClass}`).classList.remove(this.contentActiveClass) // то скрываем его
      }
      body.querySelector(`[data-tab=${this.getActiveTabName(head)}]`).classList.add(this.contentActiveClass) // затем ищем элемент контента, у которого значение data-tab совпадает со значением data-tab активной кнопки и отображаем его
    }

    onLoad(head, body) { // метод для описания логики при загрузке страницы
      // проверяем при загрузке страницы, есть ли активная вкладка
      if (!head.querySelector(`.${this.captionActiveClass}`)) { // если активной вкладки нет
        head.querySelector(this.caption).classList.add(this.captionActiveClass) // то делаем активной по-умолчанию первую вкладку
      }

      this.setActiveContent(head, body) // устанавливаем активный элемент контента в соответствии с активной кнопкой при загрузке страницы
    }

    onClick(head, body) { // метод для описания логики при клике на элемент с кнопками
      head.addEventListener('click', e => { // при клике на элемент с кнопками
        const caption = e.target.closest(this.caption) // узнаем, был ли клик на кнопке
        if (!caption) return // если клик был не на кнопке, то прерываем выполнение метода
        if (caption.classList.contains(this.captionActiveClass)) return // если клик был на активной кнопке, то тоже прерываем выполнение метода и ничего не делаем

        if (head.querySelector(`.${this.captionActiveClass}`)) { // если уже есть активная кнопка
          head.querySelector(`.${this.captionActiveClass}`).classList.remove(this.captionActiveClass) // то удаляем ей активный класс
        }

        caption.classList.add(this.captionActiveClass) // затем добавляем активный класс кнопке, на которой был клик

        this.setActiveContent(head, body) // устанавливаем активный элемент контента в соответствии с активной кнопкой
      })
    }

    init() { // основной метод для вызова других описанных методов
      const tabs = document.querySelector(this.tabs) // ищем на странице элемент по переданному селектору основного элемента вкладок и записываем в константу
      const head = tabs.querySelector(this.head) // ищем в элементе tabs элемент с кнопками по переданному селектору и записываем в константу
      const body = tabs.querySelector(this.body) // ищем в элементе tabs элемент с контентом по переданному селектору и записываем в константу

      this.onLoad(head, body) // вызываем метод onLoad и передаем в параметрах константы, объявленные выше

      this.onClick(head, body) // вызываем метод onClick и передаем в параметрах константы, объявленные выше
    }
  }

  new Tabs({ // создаем экземпляр JavaScript класса Tabs, и передаем значения CSS классов и селекторов элемента вкладок, которые нужно оживить - section__tabs
    tabsSelector: '.section__tabs', // основной элемент вкладок
    tabsHeadSelector: '.tabs__head', // элемент с кнопками
    tabsBodySelector: '.tabs__body', // элемент с контентом
    tabsCaptionSelector: '.tabs__caption', // элемент кнопки
    tabsCaptionActiveClass: 'tabs__caption_active', // активный класс кнопки
    tabsContentActiveClass: 'tabs__content_active', // активный класс элемента контента
  }).init() // вызываем основной метод init

  new Tabs({ // создаем экземпляр JavaScript класса Tabs, и передаем значения CSS классов и селекторов элемента вкладок, которые нужно оживить - about__tabs
    tabsSelector: '.about__tabs', // основной элемент вкладок
    tabsHeadSelector: '.tabs__head', // элемент с кнопками
    tabsBodySelector: '.tabs__body', // элемент с контентом
    tabsCaptionSelector: '.tabs__caption', // элемент кнопки
    tabsCaptionActiveClass: 'tabs__caption_active', // активный класс кнопки
    tabsContentActiveClass: 'tabs__content_active', // активный класс элемента контента
  }).init() // вызываем основной метод init

})


Итоги

Данный вариант реализации всего лишь один из многих и не претендует на эталонность. Вы можете использовать как этот вариант, так и другие доступные.

В данной реализации кнопки и блоки контента можно располагать не соблюдая их порядок в HTML-структуре. Eсть проверка при загрузке страницы, если не установлена активная вкладка, то устанавливаем активной первую вкладку. Также не используются циклы, что уменьшает количество событий при переключении вкладок. При использовании CSS стилей из статьи эти вкладки логично адаптируются под любое разрешение экрана

Буду рад, если статья оказалась полезной

Спасибо за ваше внимание и уделённое время!



Полезные ссылки

MDN Web Docs - Определение классов

learn.javascript.ru - Классы

learn.javascript.ru - Функции

MDN Web Docs - Шаблонные строки

CSS-переменные: как сделать темную тему