Привет, Хабр!
За последний год HTML получил деталь, которая меняет привычные «аккордеоны». У <details>
появился атрибут name
, и этим всё сказано: теперь эксклюзивные аккордеоны можно сделать без строчек JavaScript, а стили и поведение дочистить через :has()
. Поддержка стала широкой, а старые практики на дивчиках и ролях можно оставить для случаев, когда действительно нужна сложная логика.
В HTML у нас давно есть пара <details>/<summary>
. Браузер сам рисует disclosure‑виджет, умеет разворачивать содержимое, бережно обращается с фокусом и клавиатурой. Сейчас поверх этого добавился name
, который превращает набор из нескольких <details>
в группу, открываешь одно и закрываются остальные из той же группы. Если в группе вы отметили несколько элементов open
в исходнике, браузер оставит открытым первый по порядку.
Базовая разметка для эксклюзивного аккордеона без JS выглядит так:
<section class="accordion" aria-label="FAQ">
<details name="faq" open>
<summary>Как работает этот аккордеон</summary>
<div class="panel">
<p>Обычный <code><details></code>, но с атрибутом <code>name</code>. Элементы с одинаковым именем образуют группу.</p>
</div>
</details>
<details name="faq">
<summary>Поддерживается ли клавиатура</summary>
<div class="panel">
<p>Да. Фокус встаёт на <code><summary></code>, переключение по Enter или Space, навигация Tab/Shift+Tab.</p>
</div>
</details>
<details name="faq">
<summary>Можно ли держать открытым несколько</summary>
<div class="panel">
<p>Нет, в пределах одной группы с одинаковым <code>name</code> открыт будет только один.</p>
</div>
</details>
</section>
Эта разметка уже даёт рабочую эксклюзивность. Элементы группы не обязаны стоять рядом, они могут быть разбросаны по странице, браузер всё равно сведёт их в одну логическую связку по значению name
.
Дальше идём в стили. Начнём с нормализации маркера и базовой типографики. У разных движков маркер у <summary>
ведёт себя чуть по‑разному. Кросс‑браузерная практика сегодня такая: убрать встроенный маркер через ::marker
плюс ветка для WebKit.
.accordion {
--radius: .5rem;
--border: 1px solid var(--stroke, #e0e0e0);
--bg: var(--surface, #fff);
--bg-hover: #f7f7f7;
--bg-active: #f0f6ff;
--text: #111;
}
.accordion details {
border: var(--border);
border-radius: var(--radius);
background: var(--bg);
}
.accordion details + details {
margin-top: .5rem;
}
/* маркер у summary */
.accordion summary {
list-style: none;
cursor: pointer;
padding: .75rem 1rem;
font: 600 16px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--text);
}
.accordion summary::marker,
.accordion summary::-webkit-details-marker {
display: none;
}
/* свой «маркер» иконкой */
.accordion summary .caret {
inline-size: 1rem;
block-size: 1rem;
display: inline-block;
margin-inline-end: .5rem;
transition: transform .2s ease;
vertical-align: -2px;
}
.accordion .panel {
padding: 0 1rem 1rem;
}
::marker
сегодня работает для <summary>
в современных браузерах, но Safari исторически требовал ::-webkit-details-marker
.
Чтобы не городить JS ради классики «повернуть стрелку при раскрытии» и подсветить активный пункт, используем селектор :has()
и состояние [open]
у <details>
:
/* подсветка активной шапки и поворот стрелки */
.accordion details[open] > summary {
background: var(--bg-active);
}
.accordion details[open] > summary .caret {
transform: rotate(90deg);
}
/* hover и фокус для доступности */
.accordion summary:hover {
background: var(--bg-hover);
}
.accordion summary:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
border-radius: calc(var(--radius) - 2px);
}
/* стили на уровне контейнера через :has() */
.accordion:has(> details[open]) {
--stroke: #d4e2ff;
}
/* «приглушить» неактивные пункты, когда какой-то открыт */
.accordion:has(> details[open]) > details:not([open]) {
opacity: .85;
}
Поддержка :has()
сегодня широкая, и это уже рабочий инструмент не только для демо, но и для продакшена. Если нужна страховка, завернём реактивные стили в @supports(selector(:has(*)))
и дадим мягкий фолбек для старых браузеров.
@supports (selector(:has(*))) {
.accordion:has(> details[open]) {
box-shadow: 0 0 0 1px #e8eefc inset;
}
}
@supports not (selector(:has(*))) {
/* минимальный фолбек: без подсветки на контейнере, всё остальное работает */
}
Анимации. У <details>
нет встроенной плавной анимации раскрытия. На чистом CSS можно анимировать контейнер панельки, задать ему max-height
и подстраховаться для предпочтений по сниженной анимации.
.accordion .panel {
overflow: clip; /* аккуратный обрез без дорогого overflow:hidden */
max-height: 0;
transition: max-height .25s ease;
}
.accordion details[open] .panel {
max-height: 60svh; /* достаточно, чтобы «переварить» контент разумной длины */
}
@media (prefers-reduced-motion: reduce) {
.accordion .panel {
transition: none;
}
}
Клавиатура и фокус. <summary>
по умолчанию фокусируемый элемент. Пользователь нажимает Enter или Space — панель меняет состояние. Это поведение реализовано самим браузером и документировано в спецификациях и материалах по доступности. Добавлять role="button"
на <summary>
не нужно и даже вредно: некоторые браузеры и читалки уже трактуют его как кнопку, и переопределения ролей ломают семантику вложенного содержимого. Тестируйте, чтобы убедиться, что, например, заголовок внутри <summary>
не теряет роль для скринридера такие несогласованности встречаются, поэтому безопаснее держать в <summary>
обычный текст и декоративную иконку.
Теперь соберём всё вместе с маленькой, но аккуратной декоративной иконкой. SVG встроим инлайном, чтобы не плодить внешние зависимости:
<details name="faq" open>
<summary>
<span class="caret" aria-hidden="true">
<svg viewBox="0 0 16 16" width="16" height="16" focusable="false">
<path d="M5 3l6 5-6 5z"></path>
</svg>
</span>
Как работает этот аккордеон
</summary>
<div class="panel">
<p>В одном имени группы открытым остаётся только один элемент.</p>
</div>
</details>
Дальше разберём чуть менее очевидные кейсы, которые вы можете встретить в вёрстке.
Группы не обязательно рядом. name="specs"
может жить и в блоке FAQ, и в колонке с фильтрами, и в футере. Браузер всё равно синхронизирует состояние внутри одной группы. Это удобно в документации или лонгридах, где аккордеоны раскиданы по странице. Если вы оставите нескольким элементам open
в исходнике, визуально откроется только первый, ожидаемое поведение для эксклюзивной группы.
<!-- в начале страницы -->
<aside>
<details name="specs" open>
<summary>CPU</summary>
<div class="panel"><p>Подробности по CPU...</p></div>
</details>
</aside>
<!-- много контента, а потом внизу страницы снова часть той же группы -->
<footer>
<details name="specs">
<summary>Storage</summary>
<div class="panel"><p>Диски, контроллеры и т.п.</p></div>
</details>
</footer>
:has()
как крючок логики для родителя. В компонентном стиле удобно менять оформление контейнера в ответ на состояние потомков и избегать классов‑флагов. Пара приёмов:
/* у сжатых панелей убираем нижние скругления, у открытой возвращаем */
.accordion > details:not([open]) {
border-radius: var(--radius);
}
.accordion > details[open] {
border-radius: var(--radius);
}
/* тонкая разделительная линия только когда есть открытая панель */
@supports (selector(:has(*))) {
.accordion:has(> details[open]) > details[open] {
box-shadow: 0 -1px 0 0 #e6e6e6 inset;
}
}
Иконки в <summary>
лучше делать декоративными. Не вкладывайте интерактивные элементы в <summary>
, там уже есть собственное управление. Ссылки и кнопки держите внутри .panel
. Так меньше конфликтов между своими и чужими фокусируемыми элементами, и меньше сюрпризов в скринридерах.
Кросс‑браузерная поддержка. Что важно знать сейчас.
name
у<details>
поддержан в Chrome 120+, Safari 17.2+, Firefox 130+, Edge 120+, мобильные версии соответствуют десктопам. Если пользователь придёт со старым браузером, он увидит «обычные» раскрывающиеся блоки без «эксклюзивности». Ничего не сломается.:has()
доступен в актуальных движках. Для безопасного применения используйте@supports(selector(:has(*)))
, это и есть правильная проверка поддержки селектора.Маркер у
<summary>
кастомизируется, но Safari всё ещё требует::-webkit-details-marker
. Дублируем правила, как показано выше.
Деталь для печати и навигации. Часто аккордеоны встречаются в документации, которую печатают или на которую ссылаются якорями.
/* печать: раскрыть всё и не рвать панель на страницах */
@media print {
details {
border: 0;
}
details[open] .panel,
details .panel {
max-height: none !important;
}
details {
break-inside: avoid;
}
}
Про якоря: чистым CSS нельзя поставить атрибут open
на основании :target
. Поэтому прямого способа раскрыть по хэшу без JS нет. Можно подсветить нужный блок и подсказать пользователю, что он кликабельный, но именно раскрыть нет. Это поведение стандарта; если нужно автоскрытие/автораскрытие по якорю, придётся подключить скрипт.
/* выявить нужный блок через :has() и :target */
details:has(> .panel:has(> *:is(h2, h3, h4, p, div)#targeted:target)) > summary {
background: #fff7d6;
}
Доступность. Короткая памятка:
— Не вешайте роли на <summary>
. Не добавляйте aria-expanded
вручную. Браузер сам объявляет состояние. Смешивание ролей ведёт к конфликтам.
— В <summary>
держите понятный текст. Пустой <summary>
это нарушение и для клавиатуры, и для чтения с экрана.
— Фокус‑стили делайте видимыми. :focus-visible
и хорошая контрастность решают половину проблем.
Как всё это встроить в дизайн‑систему. Компонент аккордеона на <details>
хорош в случаях: FAQ, справка, небольшие фильтры, технические раскрывашки в документации. Там, где требуется сложная логика, динамические источники данных, вложенные фокусы внутри шапки, синхронизация с URL и история — используйте JS‑вариант с кнопками и ARIA паттерном «Accordion» из APG, но только когда есть реальные поведенческие требования.
Часто в аккордеоне живут формы. Полезно подкрашивать шапку, если внутри есть ошибки валидации. Это опять делает :has()
.
/* если внутри открытой панели есть невалидные поля, подсветим summary */
@supports (selector(:has(*))) {
details[open]:has(.panel :is(input:invalid, select:invalid, textarea:invalid)) > summary {
background: #fff0f0;
box-shadow: inset 0 0 0 1px #ffd6d6;
}
}
И ещё два шлифовочных штриха: «крупный клик» и «ударопрочные» границы.
/* увеличить область клика по summary без смещения контента */
.accordion summary {
position: relative;
}
.accordion summary::after {
content: "";
position: absolute;
inset: -6px;
}
/* визуально «сцепить» соседние элементы группы */
.accordion > details:not(:first-child) {
margin-top: -1px; /* схлопываем соседние бордеры */
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.accordion > details:first-child {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.accordion > details:last-child {
border-bottom-left-radius: var(--radius);
border-bottom-right-radius: var(--radius);
}
Наслаивание групп. Иногда на одной странице нужно несколько независимых эксклюзивных аккордеонов. Давайте им разные name
, иначе группы пересекутся.
<section class="accordion" aria-label="Раздел 1">
<details name="a">
<summary>Пункт A1</summary>
<div class="panel"><p>Контент A1</p></div>
</details>
<details name="a">
<summary>Пункт A2</summary>
<div class="panel"><p>Контент A2</p></div>
</details>
</section>
<section class="accordion" aria-label="Раздел 2">
<details name="b" open>
<summary>Пункт B1</summary>
<div class="panel"><p>Контент B1</p></div>
</details>
<details name="b">
<summary>Пункт B2</summary>
<div class="panel"><p>Контент B2</p></div>
</details>
</section>
Вопрос совместимости и деградации. Если по метрикам у вас заметная доля браузеров без name
у <details>
или без :has()
, то:
— аккордеон продолжит работать как неэксклюзивный;
— стили, завязанные на :has()
, благополучно пропадут;
— доступность и клавиатура не пострадают.
Для строгой эксклюзивности на старых движках можно добавить маленький progressive enhancement скриптом‑перехватчиком кликов, но это уже другой сценарий. Документация и статьи по теме подтверждают: сегодня чистый вариант уже можно закладывать по умолчанию, а JS подключать только там, где он нужен.
Итог. HTML уже даёт рабочий аккордеон с нужной эксклюзивностью, а CSS через :has()
закрывает большинство визуальных и логических трюков без скриптов. В случае, когда нужен контроль URL, сложная синхронизация состояния, телеметрия кликов на уровне кнопок, включайте JS‑вариант по мотивам ARIA APG. Во всех остальных случаях <details name>
плюс несколько аккуратных стилей — простое, надёжное и читаемое решение, которое не тянет за собой лишний код и не ломает доступность.
Сегодня всё больше привычных элементов вёрстки можно реализовать без единой строчки JavaScript — достаточно аккуратно использовать возможности самого HTML и CSS. Такой подход не только упрощает поддержку кода, но и делает его более предсказуемым и доступным.
Если вам близка эта логика — использовать нативные средства разметки и стилей максимально эффективно, — вы можете подробнее изучить тему на курсе «HTML/CSS». Помимо курса, обратите внимание на каталог курсов: там собраны программы по различным аспектам веб‑разработки.
Также рекомендуем ознакомиться с календарем бесплатных открытых уроков, где каждый сможет найти что‑то полезное для себя.
Чтобы оставаться в курсе актуальных технологий и трендов, подписывайтесь на Telegram-канал OTUS.
gmtd
А ссылку на реальный пример со всем этим кодом нельзя?
А то добиться приемлемой надежной анимации так и не получилось
winkyBrain
Вот да, тоже не хватило ссылки на какую-нибудь песочницу. Когда кода много - возникает желание его потыкать. Но здесь блог компании, в таких случаях обычно материал пишется только для того, чтобы поддержать в этом блоге активность, а не для нас с вами
Dimox
Вот пример правильной анимации, но поддержка браузерами ограниченная - https://codepen.io/geoffgraham/pen/vEBrKRO
Взято из статьи - https://css-tricks.com/using-styling-the-details-element/