Как сделать аккордеон на HTML и CSS без JavaScript: элемент details
Аккордеон собирается на элементе details: пункты раскрывает браузер, а атрибут name оставляет открытым один. Показываем код, свою стрелку, плавное раскрытие на CSS и поддержку браузеров.
Аккордеон на HTML и CSS делается без JavaScript на элементе details: заголовок пункта лежит в summary, скрытый текст идёт следом, а раскрывает пункт сам браузер. Чтобы одновременно оставался открытым только один пункт, всем details группы задаём одинаковый атрибут name (Chrome и Edge 120, Firefox 130, Safari 17.2). Плавное раскрытие добавляет CSS с ::details-content, но высоту auto анимируют пока только Chrome и Edge: в остальных браузерах пункт раскрывается сразу.
Коротко
- Пункт аккордеона - это
detailsсsummaryпервым ребёнком. Клик поsummaryоткрывает и закрывает пункт, скрипт не нужен. - Пункт открыт, пока у
detailsесть атрибутopen. Другие пункты он не трогает. - Одинаковый
nameу несколькихdetailsсобирает их в группу: открытие одного закрывает другой. Группа не требует, чтобы один пункт был открыт всегда: по клику на открытый пункт его можно закрыть. - Стандартную стрелку убираем через
list-style: noneи::-webkit-details-marker, свою рисуем в::afterуsummary. - Пункт управляется с клавиатуры: на
summaryпереходим клавишей Tab, открываем и закрываем клавишами Enter и Пробел. Мы проверили это в Chrome 154. - Плавное раскрытие даёт
::details-content(Chrome и Edge 131, Firefox 143, Safari 18.4) вместе сinterpolate-size, которое понимают только Chrome и Edge. Без него аккордеон работает так же, но без анимации.
Как сделать аккордеон на details
Аккордеон - это список пунктов, у каждого есть заголовок, а текст под ним скрыт, пока пункт не открыт. В HTML такой пункт уже есть: элемент details скрывает всё содержимое, кроме summary, и показывает его после клика по заголовку.
<details>
<summary>Что такое аккордеон?</summary>
<p>Список пунктов, у каждого заголовок и скрытый текст.</p>
</details>
<details>
<summary>Нужен ли JavaScript?</summary>
<p>Нет, пункты раскрывает сам браузер.</p>
</details>
summary ставим первым ребёнком details. Если его не будет, браузер подставит свою подпись, обычно «Details». Всё остальное внутри details - обычная разметка: абзацы, списки, ссылки, формы.
Пункт открыт, пока у details стоит булев атрибут open. Чтобы пункт был открыт при загрузке страницы, пишем <details open>. Браузер сам добавляет и убирает этот атрибут, когда пользователь кликает по заголовку. Чтобы закрыть пункт в разметке, атрибут убираем целиком: запись open="false" пункт откроет, потому что у булева атрибута значение не читается.
О каждом переключении сообщает событие toggle. Оно приходит после смены состояния, и если состояние поменялось несколько раз до отправки, события склеиваются в одно. Для аккордеона на чистом HTML событие не требуется, оно нужно, когда на открытие пункта должен реагировать наш код: например, подгружать данные.
Пример ниже показывает два набора из трёх пунктов. Набор «Без name» состоит из обычных details, набор «С name» получит группу, о ней речь в следующем разделе. Откроем в первом наборе все три пункта: они останутся открытыми одновременно. Если на узком экране наборы расположились друг под другом, это не меняет поведения. Проверить результат просто: откроем второй пункт в наборе «С name», и первый закроется сам.
Как оставить открытым только один пункт
Для аккордеона, где открыт один пункт, у каждого details группы ставим одинаковый name.
<details name="faq" open>
<summary>Что такое аккордеон?</summary>
<p>Список пунктов, у каждого заголовок и скрытый текст.</p>
</details>
<details name="faq">
<summary>Нужен ли JavaScript?</summary>
<p>Нет, пункты раскрывает сам браузер.</p>
</details>
<details name="faq">
<summary>Можно открыть сразу два?</summary>
<p>Нет, при открытии пункта предыдущий закрывается.</p>
</details>
Браузер следит за группой сам. В наборе «С name» предыдущего примера мы открывали пункты по очереди, и в Chrome 154 открытым оставался только последний. Если после этого кликнуть по уже открытому пункту, он закроется, и в группе не будет открытых пунктов: name ограничивает число открытых пунктов сверху, но не требует, чтобы один из них был открыт всегда.
Несколько тонкостей группы:
- Пункты группы не обязаны идти подряд в разметке: между ними может быть другой код. Группу задаёт значение
name, а не соседство. - На странице может быть несколько аккордеонов, если у каждого свой
name. - Если в группе несколько пунктов помечены
open, по документации MDN откроется только первый по порядку в разметке. Мы проверили в Chrome 154 запись, где первый пункт закрыт, а второй и третий сopen: открытым остался второй. - Когда мы открываем пункт кодом, действует то же правило:
details.open = trueзакрывает остальные пункты группы, а для каждого изменённого пункта приходит своё событиеtoggle.
В браузере без поддержки name атрибут игнорируется. Пункты остаются независимыми, каждый по-прежнему открывается и закрывается. Поддержка у name такая: Chrome и Edge 120, Firefox 130, Safari 17.2.
Как заменить стрелку и оформить заголовок
Браузер рисует перед summary треугольник, маркер раскрытия. По умолчанию summary ведёт себя как элемент списка со значением display: list-item, поэтому маркером управляют так же, как маркером списка: list-style: none его убирает, а ::marker меняет его вид. По документации MDN, треугольник пропадает и при смене display на block. Safari дополнительно показывает маркер через нестандартный ::-webkit-details-marker, и в нём подстраховываем правилом display: none. Свою стрелку рисуем псевдоэлементом ::after.
<section class="faq">
<details class="faq__item" name="faq" open>
<summary class="faq__question">Что такое аккордеон?</summary>
<p class="faq__answer">Список пунктов: у каждого есть заголовок, а текст под ним скрыт, пока пункт не открыт.</p>
</details>
<!-- остальные пункты такие же -->
</section>
.faq__question {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
padding: 14px 0;
cursor: pointer;
list-style: none;
}
.faq__question::-webkit-details-marker {
display: none;
}
.faq__question::after {
content: "";
flex: none;
width: 8px;
height: 8px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
transition: transform 0.2s;
}
.faq__item[open] > .faq__question::after {
transform: rotate(-135deg);
}
.faq__question:focus-visible {
outline: 2px solid #6aa9ff;
outline-offset: 2px;
}
Стрелка поворачивается по селектору [open]: браузер сам выставляет этот атрибут, поэтому состояние пункта и вид стрелки не расходятся. Рамку фокуса мы не убираем: без неё человек, который ходит по странице клавишей Tab, не видит, на каком пункте стоит.
С клавиатуры пункт управляется без дополнительного кода. В Chrome 154 мы перешли на заголовок клавишей Tab, Enter открыл пункт, Пробел закрыл его, и второй Пробел открыл снова.
Внутри summary лучше держать только текст и простую разметку без заголовков. По документации MDN, в части браузеров summary получает роль кнопки, а у вложенного h3 роль заголовка пропадает: скринридер не объявит его как заголовок. Если нужен заголовок в структуре страницы, роль придётся проверять в нужных браузерах и программах чтения.
Что учесть: поиск по странице и ограничения
У details есть ещё одно свойство: текст в закрытом пункте находится поиском по странице. По таблицам совместимости MDN поиск раскрывает пункт с найденным текстом в Chrome и Edge с версии 97, в Firefox с версии 139 и в Safari с версии 26.2. В Firefox до версии 148 и в Safari 26.2 есть ошибка: страница может не прокрутиться к найденному месту.
Есть и границы:
- Сам по себе
detailsпункт не анимирует: переключение мгновенное. Плавность добавляет CSS из следующего раздела. - Если пункту нужно сложное поведение, например подгрузка с сервера или закрытие по условию, понадобится скрипт на событии
toggle. - Чтобы развернуть или свернуть все пункты одной кнопкой, нужен скрипт со свойством
details.open. Пункты с общимnameтак вместе не откроются: останется открытым один.
Как плавно раскрывать и закрывать пункт
Браузер хранит содержимое details в отдельной части, и CSS получил для неё псевдоэлемент ::details-content. Пока пункт закрыт, у этой части стоит content-visibility: hidden, а при открытии оно меняется на visible. В Chrome 154 мы прочитали это значение у закрытого и открытого пункта через getComputedStyle: hidden и visible. Без перехода содержимое при закрытии исчезает мгновенно, поэтому плавность строится на трёх приёмах:
block-size: 0иoverflow: clipу закрытого пункта иblock-size: autoу открытого задают, откуда и докуда меняется высота;interpolate-size: allow-keywordsразрешает анимировать высоту между числом и ключевым словомauto, как в статье «Плавное появление блока в CSS: как анимировать display: none»;content-visibilityв списке переходов с ключевым словомallow-discreteудерживает содержимое видимым, пока идёт закрытие.
.faq__answer {
margin: 0;
padding: 0 0 14px;
}
@supports (interpolate-size: allow-keywords) {
:root {
interpolate-size: allow-keywords;
}
.faq__item::details-content {
block-size: 0;
overflow: clip;
transition: block-size 0.25s ease, content-visibility 0.25s allow-discrete;
}
.faq__item[open]::details-content {
block-size: auto;
}
}
@media (prefers-reduced-motion: reduce) {
.faq__question::after,
.faq__item::details-content {
transition: none;
}
}
Правило .faq__answer с margin: 0 здесь нужно: у абзаца по умолчанию есть внешние поля, и если их оставить, пункт в конце перехода подпрыгивает. В Chrome 154 высота пункта в последних кадрах менялась с 116,5 до 133 пикселей, а с margin: 0 росла плавно до 115. Нижний отступ задаём через padding. В пене к коду добавлены общие стили страницы и рамки между пунктами, на поведение они не влияют.
Мы замерили в Chrome 154 высоту ::details-content у пункта с абзацем в 62 пикселя. При открытии она выросла от 0 до 62 пикселей с промежуточными значениями 18, 48 и 57 пикселей, а при закрытии вернулась к 0, и содержимое оставалось видимым до конца перехода.
Запасной вариант мы вынесли в условие @supports. Поддержка у частей разная: ::details-content понимают Chrome и Edge 131, Firefox 143 и Safari 18.4, а interpolate-size пока только Chrome и Edge 129. В Firefox и Safari условие ложно, блок правил не применяется, и пункт работает как обычный details: открывается и закрывается сразу. Мы имитировали такой случай в Chrome, заменив условие на заведомо ложное: пункт открывался и закрывался сразу, без промежуточных значений высоты. В самих Firefox и Safari мы пример не запускали и опираемся на таблицы совместимости. Для читателей с настройкой «уменьшить движение» блок prefers-reduced-motion отключает переходы.
Ниже итоговый аккордеон: группа через name, своя стрелка, плавное раскрытие и запасной вариант. Чтобы рассмотреть анимацию, откроем пример в редакторе Penlab и поставим в обоих значениях transition у .faq__item::details-content по 1s вместо 0.25s.
Свой аккордеон проще всего собрать в новом пене: он бесплатный, а готовый пример можно отправить ссылкой. Родственный нативный элемент для окон поверх страницы разобран в статье «Как сделать модальное окно на HTML, CSS и JS».
FAQ
Нужен ли JavaScript для аккордеона?
Нет. details открывается и закрывается сам, атрибут name делает аккордеон из одного открытого пункта, а плавность даёт CSS. Скрипт нужен, когда код должен реагировать на открытие пункта: для этого есть событие toggle.
Как открыть пункт по умолчанию?
Ставим атрибут open: <details open>. Если в группе с одним name открыты несколько пунктов, по документации MDN откроется первый по порядку в разметке.
Можно ли закрыть все пункты группы?
Да. Группа с name разрешает не больше одного открытого пункта, но не требует, чтобы один был открыт. В Chrome 154 клик по открытому пункту закрыл его, и в группе не осталось открытых.
Как открыть пункт из JavaScript?
Записываем details.open = true. В группе с name остальные пункты при этом закрываются. Мы проверили в Chrome 154: пункт открылся, предыдущий закрылся, и для каждого пришло событие toggle.
Что будет в старом браузере?
Атрибут name без поддержки игнорируется: пункты работают по отдельности. Блок с ::details-content в нашем коде лежит под @supports, поэтому без поддержки interpolate-size пункт раскрывается без анимации.
Почему открытие плавное, а закрытие мгновенное?
Стоит проверить список переходов: в нём должен быть content-visibility с allow-discrete. По документации MDN, без такого перехода содержимое при закрытии исчезает мгновенно.