Аккордеон

Компонент, где по клику на заголовок раскрывается/закрывается блок контента с плавной анимацией высоты.

Задача

Нужен FAQ или секция с разворачиваемыми ответами. Одновременно открыт только один пункт (поведение аккордеона).

Решение

Вариант 1 — нативный <details> (без JS)

<div class="accordion">
  <details class="accordion__item">
    <summary class="accordion__trigger">Вопрос первый</summary>
    <div class="accordion__content">
      <p>Ответ на первый вопрос. Текст может быть длинным.</p>
    </div>
  </details>
  <details class="accordion__item">
    <summary class="accordion__trigger">Вопрос второй</summary>
    <div class="accordion__content">
      <p>Ответ на второй вопрос.</p>
    </div>
  </details>
</div>
.accordion__item {
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  margin-bottom: 8px;
  overflow: hidden;
}

.accordion__trigger {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  cursor: pointer;
  font-weight: 600;
  list-style: none; /* убрать стандартный маркер */
  user-select: none;
}

.accordion__trigger::after {
  content: '▾';
  transition: transform 0.3s;
}

details[open] .accordion__trigger::after {
  transform: rotate(-180deg);
}

.accordion__content { padding: 0 20px 16px; color: #475569; }

Вариант 2 — JS с анимацией высоты через max-height

<div class="accordion" id="faq">
  <div class="accordion__item">
    <button class="accordion__trigger" aria-expanded="false">
      Вопрос 1 <span class="accordion__icon"></span>
    </button>
    <div class="accordion__panel" hidden>
      <div class="accordion__content"><p>Ответ на вопрос 1.</p></div>
    </div>
  </div>
  <!-- ещё пункты -->
</div>
const accordion = document.getElementById('faq');

accordion.addEventListener('click', (e) => {
  const trigger = e.target.closest('.accordion__trigger');
  if (!trigger) return;

  const item    = trigger.closest('.accordion__item');
  const panel   = item.querySelector('.accordion__panel');
  const isOpen  = trigger.getAttribute('aria-expanded') === 'true';

  // Закрыть все пункты
  accordion.querySelectorAll('.accordion__trigger').forEach((t) => {
    t.setAttribute('aria-expanded', 'false');
    t.closest('.accordion__item').querySelector('.accordion__panel').hidden = true;
  });

  // Открыть текущий (если был закрыт)
  if (!isOpen) {
    trigger.setAttribute('aria-expanded', 'true');
    panel.hidden = false;
  }
});

Ключевые моменты

  • <details> + <summary> — нативное решение без JS; ограничение: нет анимации и нет принудительного «один открыт».
  • aria-expanded на кнопке — обязателен для screen reader.
  • При hidden атрибуте элемент скрыт; для анимации используй max-height + transition или Web Animations API.
  • Делегирование клика на родительский .accordion — один обработчик для любого числа пунктов.

Варианты

  • Tabbed interface вместо аккордеона — если пункты можно просматривать все сразу.
  • CSS @keyframes + grid-template-rows: 0fr → 1fr — современная анимация раскрытия без JS.

Связанные рецепты / темы