Привет, Хабр!
За последний год 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.
ссылка на оригинал статьи https://habr.com/ru/articles/939362/
Добавить комментарий