Атрибут aria-expanded – состояние (state) из спецификации WAI-ARIA (см. ARIA), которое сообщает вспомогательным технологиям, раскрыт или свёрнут блок, управляемый элементом. Зрячий пользователь видит, что панель аккордеона закрыта, а мобильное меню спрятано; скринридер без атрибута объявит просто «кнопка» – и пользователь не узнает ни того, что кнопка что-то разворачивает, ни текущего состояния. С aria-expanded та же кнопка озвучивается как «кнопка, свёрнуто» или «кнопка, развёрнуто» – состояние понятно до клика. Это обязательный атрибут в интерактивных паттернах доступности (a11y): аккордеон, раскрывашка (disclosure), выпадающее и бургер-меню, combobox, разворачиваемые строки таблиц.
Как это работает
Ключевое правило, в котором ошибаются чаще всего: aria-expanded ставится на управляющий элемент – фокусируемую кнопку-триггер, а не на раскрываемую панель. Атрибут описывает контрол («эта кнопка разворачивает контент, сейчас он скрыт»), а сам блок прячут обычными средствами – атрибутом hidden или display:none.
<!-- Ошибка: атрибут на панели -->
<button>Подробнее</button>
<div class="panel" aria-expanded="true">…</div>
<!-- Правильно: атрибут на кнопке -->
<button aria-expanded="false" aria-controls="panel">Подробнее</button>
<div id="panel" hidden>…</div>| Значение | Что означает |
|---|---|
| true | Управляемый блок раскрыт и отображается |
| false | Управляемый блок свёрнут; в закрытом состоянии значение проставляют явно, а не удаляют атрибут |
| Атрибута нет | Состояние undefined: элемент вообще не управляет раскрываемым блоком |
Семантика undefined важна на практике. Атрибут нельзя вешать «на всякий случай» на элементы, которые ничем не управляют: само его наличие заявляет скринридеру, что перед пользователем раскрывающий контрол. По той же причине в дереве (tree) true или false несут только родительские узлы, а листья остаются без атрибута – иначе они будут объявлены «родителями» без потомков. Исключение из правила «нет блока – нет атрибута» одно: у combobox значение по умолчанию – aria-expanded="false", свёрнутый список считается нормальным стартовым состоянием.
aria-expanded и aria-controls – как связаны
aria-expanded отвечает на вопрос «раскрыто ли», aria-controls – «что именно»: в него записывают id управляемой панели. Требования W3C ARIA Authoring Practices Guide (APG) различаются по паттернам: в аккордеоне aria-controls на кнопке-заголовке обязателен, в простом disclosure («показать ещё») – опционален. Переключение состояния – всегда работа JavaScript: по клику скрипт меняет true на false и обратно и синхронно прячет или показывает панель. Если содержимое раскрывается через полную перезагрузку страницы, динамическое переключение не нужно.
Зачем это нужно
Главная ценность – предсказуемость интерфейса. Пользователь скринридера слышит состояние до взаимодействия, а после клика получает подтверждение, что действие сработало: фокус остаётся на кнопке, и смена озвучки со «свёрнуто» на «развёрнуто» – единственный внятный сигнал, что панель открылась. Типовые сценарии: аккордеон в FAQ-блоке, бургер и выпадающие подменю в шапке, фильтры каталога «показать все», строки таблиц и деревья. Побочные выгоды для разработчика:
- Стилизация без лишних классов – состояние уже в DOM, поэтому иконку-стрелку поворачивают селектором атрибута:
button[aria-expanded="true"] .chevron { transform: rotate(180deg); }. Скрипт меняет один атрибут, а не жонглирует классами вроде .is-open. - Нативная альтернатива – элементы details/summary сообщают состояние раскрытия сами, дублировать их aria-expanded не нужно. Самодельный вариант на кнопке выбирают, когда нужен контроль над анимацией, разметкой или логикой «открыта только одна панель».
- Машиночитаемость – явные состояния и связи по id делают структуру страницы понятнее любым парсерам, а не только скринридерам: та же логика семантики, что работает на GEO.
Пример
Аккордеон для FAQ-блока по паттерну APG – связка, которую я использую на Битрикс-проектах вместе с разметкой FAQPage: видимая часть доступна скринридерам, машиночитаемая – в JSON-LD. Заголовок – элемент h3, внутри него кнопка; панель связана с кнопкой через aria-labelledby:
<h3>
<button aria-expanded="false" aria-controls="faq1-panel" id="faq1-btn">
Сколько длится разработка сайта?
</button>
</h3>
<div id="faq1-panel" role="region" aria-labelledby="faq1-btn" hidden>
<p>Обычно от четырёх недель…</p>
</div>Панель получает role="region" и aria-labelledby на кнопку – скринридер связывает содержимое с вопросом. По APG, если раскрытую панель нельзя свернуть (например, последняя открытая), на её кнопку добавляют aria-disabled="true", а не блокируют элемент атрибутом disabled, чтобы не выкидывать его из фокуса. JavaScript-обработчик читает текущее состояние, инвертирует его и синхронно переключает панель:
btn.addEventListener('click', () => {
const open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
panel.hidden = open;
});Второй частый случай – бургер мобильного меню. У кнопки нет текста, поэтому доступное имя задаёт aria-label, а состояние – aria-expanded:
<button aria-expanded="false" aria-controls="site-nav" aria-label="Открыть меню">
<svg aria-hidden="true">…</svg>
</button>
<nav id="site-nav" hidden>…</nav>В закрытом состоянии панель спрятана атрибутом hidden – меню недоступно и визуально, и для скринридера, пока кнопка не переключит состояние. Клавиатура в обоих паттернах работает нативно: Enter и Space на кнопке переключают панель, Tab идёт по обычному порядку, перехватывать стрелки не нужно. Внутри раскрытого меню активный пункт помечают атрибутом aria-current – три атрибута вместе закрывают базовую доступность навигации сайта.