Привет, Хабр!

За последний год 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>&lt;details&gt;</code>, но с атрибутом <code>name</code>. Элементы с одинаковым именем образуют группу.</p>
    </div>
  </details>

  <details name="faq">
    <summary>Поддерживается ли клавиатура</summary>
    <div class="panel">
      <p>Да. Фокус встаёт на <code>&lt;summary&gt;</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. Так меньше конфликтов между своими и чужими фокусируемыми элементами, и меньше сюрпризов в скринридерах.

Кросс‑браузерная поддержка. Что важно знать сейчас.

  1. name у <details> поддержан в Chrome 120+, Safari 17.2+, Firefox 130+, Edge 120+, мобильные версии соответствуют десктопам. Если пользователь придёт со старым браузером, он увидит «обычные» раскрывающиеся блоки без «эксклюзивности». Ничего не сломается.

  2. :has() доступен в актуальных движках. Для безопасного применения используйте @supports(selector(:has(*))), это и есть правильная проверка поддержки селектора.

  3. Маркер у <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.

Комментарии (3)


  1. gmtd
    28.08.2025 01:09

    А ссылку на реальный пример со всем этим кодом нельзя?
    А то добиться приемлемой надежной анимации так и не получилось


    1. winkyBrain
      28.08.2025 01:09

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


    1. Dimox
      28.08.2025 01:09

      Вот пример правильной анимации, но поддержка браузерами ограниченная - https://codepen.io/geoffgraham/pen/vEBrKRO

      Взято из статьи - https://css-tricks.com/using-styling-the-details-element/