Как сверстать аккордеон FAQ

Эта статья основана на одном из практических заданий verstaem.online.

Вы можете читать материал как обычное руководство или сначала попробовать выполнить задание самостоятельно, а затем сравнить свое решение с подробным разбором.

Перейти к заданию

В этой статье мы создадим блок с часто задаваемыми вопросами (FAQ) в формате аккордеона. На его примере разберем, как оформить список вопросов, скрывать и показывать ответы с помощью 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>Аккордеон FAQ</title>
  </head>
  <body>
  </body>
</html>

Подключаем стили и JavaScript. Скрипт подключаем с атрибутом defer, чтобы он выполнился только после построения HTML-дерева.

<head>
  ...
  <link rel="stylesheet" href="css/style.css">
  <script src="js/script.js" defer></script>
</head>

Теперь переходим к разметке самого блока. В качестве контейнера используем обычный <div>, внутри которого разместим заголовки страницы, описание и список вопросов.

Для списка отлично подходит элемент <ul>, а каждый вопрос будет отдельным элементом <li>.

<div class="content">
  ...
  <ul class="accordion">
    ...
  </ul>
</div>

Каждый элемент аккордеона состоит из двух частей:

  • кнопки, по которой пользователь будет нажимать;
  • скрытого блока с ответом.

Поэтому внутри <li> размещаем <button> и блок с ответом.

<li class="accordion__item">
  <button class="accordion__control">
    Можно ли посетить все выставки по одному билету?
  </button>

  <div class="accordion__content">
    <div class="accordion__content-inner">
      Да. Единый билет действует на все публичные экспозиции фестиваля в течение выбранной даты.
    </div>
  </div>
</li>

Обратите внимание, что текст ответа находится во внутреннем блоке .accordion__content-inner. Это сделано специально. Внешний контейнер будет отвечать только за анимацию раскрытия, а внутренний — за отступы и оформление текста.

По аналогии добавляем остальные вопросы. В итоге получаем полностью семантическую разметку FAQ.

<div class="content">
  <h1 class="content__title">
    Фестиваль современного искусства
  </h1>
  <h2 class="content__subtitle">
    Часто задаваемые вопросы
  </h2>
  <p class="content__descr">
    Перед посещением выставок ознакомьтесь с ответами на самые популярные вопросы.
  </p>
  <ul class="accordion">
    <li class="accordion__item">
      <button class="accordion__control">
        Можно ли посетить все выставки по одному билету?
      </button>
      <div class="accordion__content">
        <div class="accordion__content-inner">
          Да. Единый билет действует на все публичные экспозиции фестиваля в течение выбранной даты.
        </div>
      </div>
    </li>
    <li class="accordion__item">
      <button class="accordion__control">
        Нужно ли заранее записываться на экскурсии?
      </button>
      <div class="accordion__content">
        <div class="accordion__content-inner">
          Большинство экскурсий доступны без записи, однако авторские маршруты требуют предварительного бронирования через личный кабинет.
        </div>
      </div>
    </li>
    <li class="accordion__item">
      <button class="accordion__control">
        Можно ли фотографировать экспонаты?
      </button>
      <div class="accordion__content">
        <div class="accordion__content-inner">
          Фотосъемка разрешена в большинстве залов без использования вспышки.
        </div>
      </div>
    </li>
    <li class="accordion__item">
      <button class="accordion__control">
        Есть ли скидки для студентов?
      </button>
      <div class="accordion__content">
        <div class="accordion__content-inner">
          Да, при предъявлении действующего студенческого билета.
        </div>
      </div>
    </li>
    <li class="accordion__item">
      <button class="accordion__control">
        Можно ли вернуть билет?
      </button>
      <div class="accordion__content">
        <div class="accordion__content-inner">
          Возврат возможен не позднее чем за семь дней до открытия фестиваля.
        </div>
      </div>
    </li>
  </ul>
</div>

Добавляем стили

Подключим файл стилей через <link> и зададим несколько базовых правил. Для этого задания достаточно переопределить box-sizing, сбросить отступ у <body> и подключить шрифт IBM Plex Sans, который используется в макете.

* {
  box-sizing: border-box;
}

@font-face {
  font-family: "IBM Plex Sans";
  src: url("../fonts/IBMPlexSansRegular.woff2");
  font-display: swap;
  font-weight: 400;
}

@font-face {
  font-family: "IBM Plex Sans";
  src: url("../fonts/IBMPlexSansMedium.woff2");
  font-display: swap;
  font-weight: 500;
}

@font-face {
  font-family: "IBM Plex Sans";
  src: url("../fonts/IBMPlexSansBold.woff2");
  font-display: swap;
  font-weight: 700;
}

body {
  margin: 0;
  font-family: "IBM Plex Sans", sans-serif;
}

Теперь оформим основной контейнер страницы. Здесь все довольно просто:

  • ограничиваем максимальную ширину;
  • центрируем блок;
  • добавляем внутренние отступы.
.content {
  margin: 0 auto;
  padding: 80px 15px;
  max-width: 930px;
}

После этого оформляем заголовки и описание страницы, просто перенося размеры, цвета и отступы из макета.

.content__title {
  margin: 0;
  margin-bottom: 16px;
  font-weight: 400;
  font-size: 14px;
  line-height: 120%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #8d867b;
}

.content__subtitle {
  margin: 0;
  margin-bottom: 16px;
  max-width: 520px;
  font-weight: 700;
  font-size: 56px;
  line-height: 120%;
  color: #1f1f1f;
}

.content__descr {
  margin: 0;
  margin-bottom: 16px;
  max-width: 560px;
  font-weight: 400;
  font-size: 20px;
  line-height: 120%;
  color: #6c655b;
}

Переходим к самому аккордеону. Убираем стандартные маркеры списка, обнуляем отступы и сразу создаем CSS-счетчик. Именно он позволит автоматически нумеровать вопросы без дополнительной разметки.

.accordion {
  counter-reset: accordion 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

Далее оформляем каждый элемент списка. Добавляем разделительные линии сверху и снизу.

.accordion__item {
  border-bottom: 1px solid #d8d2c8;
}

.accordion__item:first-child {
  border-top: 1px solid #d8d2c8;
}

Теперь переходим к кнопке. Здесь используется Flexbox. С его помощью расположим номер, текст и иконку.

.accordion__control {
  position: relative;
  display: flex;
  align-items: center;
  gap: 32px;
  border: none;
  padding: 28px 0;
  width: 100%;
  font-family: inherit;
  font-weight: 500;
  font-size: 20px;
  line-height: 120%;
  text-align: left;
  color: #1f1f1f;
  background: transparent;
  cursor: pointer;
}

.accordion__control::before {
  counter-increment: accordion;
  content: counter(accordion);
  flex-shrink: 0;
  font-weight: 400;
  font-size: 12px;
  line-height: 120%;
  color: #a19a90;
}

.accordion__control::after {
  content: "";
  flex-shrink: 0;
  margin-left: auto;
  display: block;
  width: 24px;
  height: 24px;
  background-image: url("../img/cross.svg");
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  transition: rotate 0.5s;
}

.accordion__item:nth-child(-n+9) .accordion__control::before {
  content: "0" counter(accordion);
}

Здесь важный момент — это создание цифры. Мы используем псевдоэлемент и css-счетчик, чтобы не писать цифры постоянно самому. Они автоматически будут создаваться, и тут важно, как именно мы используем счетчик — задаем всем элементам с 1 до 9 число с нулем, а остальным — без нуля.

Иконку плюсика тоже делаем через css, чтобы не плодить лишний html-код.

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

.accordion__content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.5s;
}

Почему именно max-height, а не display: none? Все просто. Свойство display невозможно анимировать, а вот max-height отлично подходит для плавного раскрытия блока. Сам текст ответа получает обычные внутренние отступы и типографику.

.accordion__content-inner {
  padding-right: 80px;
  padding-left: 48px;
  padding-bottom: 32px;
  font-weight: 400;
  font-size: 16px;
  line-height: 120%;
  color: #5d564d;
}

.accordion__item.open .accordion__content {
  max-height: 1000px;
}

Когда элемент открыт, появляется класс .open. Через него увеличиваем высоту блока. Указываем 1000 как просто большое число. Но мы заменим его на правильное позже, с помощью JS.

.accordion__item.open .accordion__control::after {
  rotate: 45deg;
}

С помощью того же класса .open поворачиваем стрелочку, применяя свойство rotate.

Добавляем JavaScript

Переходим к логике. Сначала получаем весь список вопросов.

const accordion = document.querySelector('.accordion');

Вместо обработки каждого элемента отдельно используем делегирование событий. Это позволит добавить всего один обработчик.

accordion.addEventListener('click', ({ target }) => {

});

Первым делом ищем кнопку, по которой кликнул пользователь. Если клик был не по кнопке — просто завершаем выполнение функции.

const control = target.closest('.accordion__control');

if (!control) return;

Теперь получаем текущий элемент списка и проверяем, открыт ли он.

const item = control.closest('.accordion__item');
const isOpen = item.classList.contains('open');
const content = item.querySelector('.accordion__content');

Если элемент уже открыт — закрываем его. Используем if (isOpen) {}.

content.style.maxHeight = '0';
item.classList.remove('open');

Если закрыт — наоборот открываем.

item.classList.add('open');
content.style.maxHeight = content.scrollHeight + 'px';

Обратите внимание на использование свойства scrollHeight. Именно оно позволяет получить реальную высоту содержимого независимо от количества текста. Поэтому аккордеон будет корректно работать даже если ответы имеют разную длину.

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

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

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