Интерактив без скрипта: details, dialog и inert

разметка≈ 12 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Вопросы, ответы на которые раскрываются по щелчку, окно «Черновик сохранён» с кнопкой «Понятно», часть страницы, замороженная на время оплаты, — всё это привыкли писать скриптом, и обычно скрипт этот живёт в каждом проекте заново. Но в HTML для таких вещей есть готовые элементы: <details>, <dialog> и атрибут inert. Соберём раскрывающийся ответ сначала из заголовка с абзацем и посчитаем, скольких заплат он требует, а потом встретим <details>. Закроем окно формой без единой строки скрипта, выключим целую область одним атрибутом — и отделим то, что уже работает везде, от того, что пока требует скрипта или появилось слишком недавно.

Разделы темы:·

У магазина есть страница «Вопросы и ответы»: доставка, оплата, возврат, и под каждым вопросом абзац ответа. Пока вопросов три, их пишут подряд. Когда их тридцать, страница вытягивается на несколько экранов, и ответы хочется спрятать: пусть человек видит список вопросов и открывает только тот, что ему нужен.

Соберём такой вопрос из подручного: заголовок <h3> и абзац под ним. Правило .answer { display: none; } прячет ответ — точка в адресе означает класс, а display: none убирает элемент со страницы целиком: он не рисуется и места не занимает. Осталось научить вопрос открывать ответ. Проверка щёлкнет по вопросу, спросит у ответа его display и попробует поставить на вопрос фокус — отметку «клавиатура сейчас здесь». Прогноз: изменится ли display после щелчка и примет ли вопрос фокус? Ответ — в разборе после запуска.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Щелчок до заголовка дошёл, но своего действия у <h3> нет: ответ как был none, так и остался. Фокус заголовок тоже не принял, значит, Tab его пропустит.

Чтобы самоделка заработала, пришлось бы писать скрипт, который по щелчку меняет display, давать вопросу tabindex, учить его Enter и Пробелу и отдельно сообщать программе чтения с экрана, открыт ответ или закрыт. Четыре заплаты, и каждую легко забыть — молча, потому что страница при этом продолжает выглядеть правильно.

Каждая из четырёх заплат забывается молча. Не поставили tabindex — человек с клавиатуры до вопроса не дойдёт, а с мышью всё работает. Не научили Enter и Пробелу — то же самое: на проверке мышью не всплывёт. Не сообщили программе чтения, открыт ответ или закрыт, — человек услышит «Доставка?» и не узнает, что под ним есть ответ. Именно поэтому готовые элементы выигрывают не объёмом кода, а тем, что забыть в них нечего.

В HTML такой элемент уже есть. <details> — блок, который раскрывается, <summary> первым внутри него — вопрос, который видно всегда. Всё остальное внутри <details> — ответ: браузер показывает его, только когда блок открыт. Атрибут open в разметке открывает блок сразу.

Ниже два таких блока с классом faq: первый закрыт, второй открыт. Пробел в адресе значит «внутри», поэтому .faq summary — это вопросы внутри блоков. Высоту вопроса и ответа задал стиль, 30 и 40 пикселей, чтобы числа не зависели от шрифта, а margin: 0 убрал отступы, которые браузер ставит абзацу сам. Прогноз: какой высоты окажутся блоки до щелчка по вопросу и после него? Числа назовёт разбор.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
До щелчка первый блок — 30 пикселей, один вопрос, второй — 70: вопрос и ответ. Щелчок открыл первый блок, и оба стали по 70.

Скрипта в разметке нет: щелчок по <summary> браузер обрабатывает сам и ставит блоку атрибут open, а следующий щелчок атрибут снимает. Треугольник перед вопросом тоже рисует браузер — у закрытого блока он смотрит вправо, у открытого вниз, — и перерисовывает его сам. Ровно та работа, которую в самоделке делали бы вы.

Клавиатуру прибор не заменит: нажатие, отправленное из кода, действия браузера не вызывает. Поэтому руками. Щёлкните в рамке по вопросу «Доставка?» — ответ свернётся, а фокус встанет на вопрос. Нажмите Enter — ответ развернётся. Пробел свернёт его снова.

Что умеет<h3> и абзац<details> и <summary>
Прячет ответда, стилемда
Открывает ответ щелчкомнетда
Вопрос принимает фокуснетда
Открывает ответ по Enter и Пробелунетда
Хранит состояние в разметкенетда, атрибутом open
Сообщает программе чтения, открыт ли ответнетда

Последняя строка таблицы прибором не измеряется, но её видно в дереве доступности — карте страницы, которую браузер отдаёт программам чтения: вопрос там значится раскрывающимся, с пометкой, свёрнут он или развёрнут. У <details> и <summary> статус Baseline Widely available с 15 июля 2022 года — пишите.

Проверять, открыт ли блок, так и тянет по ответу: закрыт — значит, у абзаца нет высоты. Возьмём ту же разметку, уже без щелчка, и спросим про сами абзацы — .faq p, то есть абзацы внутри блоков, — а заодно про свойство open у самих блоков: замер умеет читать не только стили, но и свойства элементов. Прогноз: какой высоты окажется абзац закрытого ответа?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
У закрытого ответа те же 40 пикселей, и display у него block, как у открытого. В третьей строке open — false у первого блока и true у второго: свойство повторяет атрибут — где в разметке стоит open, там и true.

Закрытый <details> не удаляет ответ и не меняет ему стиль: браузер держит содержимое в отдельном отсеке, который не рисует и которому не отводит места. Абзац в отсеке сохраняет свои размеры, а высота всего блока складывается из одного вопроса. Отсюда правило: об открытости спрашивают сам <details> — честно отвечают его высота и свойство open.

Пока блок закрыт, ссылки и кнопки в его ответе фокус не принимают, и Tab до них не дойдёт.

Второе частое поведение — окно поверх страницы: «Черновик сохранён», «Точно удалить?», выбор города. Для него есть <dialog>. Без атрибута open окно спрятано, с атрибутом — показано. Ниже два одинаковых окна с кнопкой, открыто только второе; проверка спросит у обоих display и попробует поставить фокус на их кнопки. Прогноз: каким окажется display у каждого окна и примут ли фокус кнопки?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Закрытое окно спрятано через display: none — так же, как ответ закрытого <details>, — и его кнопка выпала из очереди Tab. Открытое окно — блок, и кнопка в нём фокус принимает.

В рамке примера открытое окно стоит посередине по ширине, обведённое чёрной линией, — это стили браузера.

Открыть окно разметкой просто. Интереснее закрыть его без скрипта. Для этого внутрь кладут форму с method="dialog": такая форма ничего не отправляет на сервер, а закрывает окно, в котором стоит. Значение value нажатой кнопки браузер записывает в свойство окна returnValue, и потом видно, какой именно кнопкой окно закрыли. Прогноз для двух строк разбора: открыто ли окно после щелчка по кнопке и что окажется в returnValue?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Окно закрылось, в returnValue — ok. Строки «форма отправлена» нет: отправки не было, форма только закрыла окно.

Если кнопок две — «Удалить» с value="delete" и «Оставить» с value="keep", — returnValue скажет, какую выбрали. Прочитать ответ и что-то с ним сделать — уже работа скрипта, но открывается и закрывается окно без него. У <dialog> статус Baseline Widely available с 14 сентября 2024 года.

Окна привыкли закрывать и клавишей Esc. Проверьте руками: уберите из примера строку с press и запустите его снова. Щёлкните по окну рядом с кнопкой и нажмите Esc — окно останется на месте. Щелчок по «Понятно» его закроет.

По умолчанию Esc закрывает только модальное окно — такое, которое на время отключает всю остальную страницу. Модальным окно делает метод showModal(), а метод вызывают из скрипта. Модальное окно встаёт поверх всего, слегка затемняет страницу под собой, переносит фокус внутрь и закрывается по Esc, а щелчки и Tab мимо него до страницы не доходят. Окно с атрибутом open ничего этого не делает: оно просто показано.

Модальные окна есть у браузера и свои — alert и confirm, и поменять их внешний вид страница не может. Их разбирает часть I, статья про диалоги браузера.

Модальное окно отключает всю страницу под собой. Бывает, что отключить нужно только часть: форму, пока идёт оплата, или меню, спрятанное за краем экрана, куда Tab всё равно заведёт человека. Для этого есть атрибут inert — «инертный», безучастный: всё внутри элемента с ним перестаёт отзываться. Ниже <main> с этим атрибутом, в нём кнопка и ссылка, а снаружи ещё одна кнопка. Прогноз: где будет «да», а где «нет»?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Кнопка и ссылка внутри <main inert> фокус не приняли, кнопка снаружи — приняла.

Одним атрибутом выключено всё содержимое, какие бы теги в нём ни стояли.

Похожее умеет disabled на группе полей <fieldset> — его разбирала статья про кнопки, ссылки и подписи. Но разница есть, и она принципиальная. Поставим ту же пару, кнопку и ссылку, сначала в выключенную группу, потом в инертный блок — и спросим, кто из четырёх элементов примет фокус и сколько окажутся выключенными.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Фокус приняла только ссылка в выключенной группе: disabled выключает поля формы, а ссылка — не поле. Кнопка и ссылка в инертном блоке не приняли ничего. :disabled насчитал двоих — группу и её кнопку.

inert отгораживает всё содержимое, какие бы теги там ни стояли, а disabled — только поля формы. Кнопка в инертном блоке выключенной не считается: inert не меняет состояние элементов, он отгораживает их от человека. Отсюда практическое правило: inert — для области, disabled — для поля.

Выбор между ними короткий. Нужно выключить область — форму на время оплаты, уехавшее за край меню, шаг мастера, который ещё не наступил, — это inert на контейнере: один атрибут вместо обхода всех полей внутри. Нужно выключить конкретное поле или группу полей, оставив человеку понимание, что здесь что-то есть, — это disabled, и причину стоит написать рядом текстом.

Щелчок мыши проходит инертную область насквозь. Проверим topAt(x, y): она отвечает, какой элемент лежит сверху в точке сцены, считая от её левого верхнего угла, или «пусто», если там ничего нет. Стиль дал кнопкам ширину 100 и высоту 30 пикселей и поставил их друг под другом, так что точка 50, 15 приходится на первую кнопку, а 50, 45 — на вторую. Прогноз: какой элемент найдёт прибор в каждой точке?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
В точке инертной кнопки — «пусто»: для щелчка она прозрачна, и попасть в неё мышью или пальцем нельзя. Вторая точка нашла живую кнопку снаружи.

Программе чтения с экрана инертное содержимое тоже недоступно: браузер убирает его из дерева доступности. Поэтому спрятанное меню не читается вслух как часть страницы — человек не услышит список ссылок, до которых всё равно не добраться.

press щёлкает из кода, и inert его не остановит: атрибут отгораживает область от человека, а не от скрипта. Если press по инертной кнопке отправит форму, прибор не ошибся.

Выглядит инертная область как обычная: браузер её не приглушает. Если человек видит кнопку, которая не нажимается, приглушите область стилем и напишите рядом, почему. У inert статус Baseline Widely available с 11 октября 2025 года — пишите. Но не все браузеры, выпущенные до весны 2023 года, его знают: в них область останется живой.

Ставить и снимать inert и open на ходу — выключить форму на время оплаты и включить после — это уже скрипт. Как он меняет атрибуты и чем атрибут отличается от свойства, покажет часть II, статья про атрибуты и свойства.

Граница «без скрипта» сдвигается: браузеры добавляют поведение, которое раньше писали руками. Но свежее доходит до читателей не сразу. Мерило то же, что для любого тега: Widely — пишите, Newly — знайте, но не делайте единственным путём, Limited — не рассчитывайте.

ВозможностьЧто делаетBaselineКак быть
<details> и <summary>раскрывают ответ щелчком и с клавиатурыWidely с 15.07.2022пишите
<dialog> и method="dialog"окно, которое закрывает формаWidely с 14.09.2024пишите
inertотгораживает область от фокуса, щелчков и программы чтенияWidely с 11.10.2025пишите
showModal()модальное окно: Esc, фокус внутри, остальная страница отключенаWidely вместе с <dialog>нужен скрипт
name у <details>открытый блок закрывает остальные с тем же именемNewly с 03.09.2024знайте
popoverвсплывающий элемент, который открывает кнопкаNewly с 27.01.2025знайте
command и commandforкнопка открывает и закрывает окно без скриптаNewly с 12.12.2025знайте

name собирает блоки <details> в группу: у нескольких блоков одно имя, например name="faq", и открытие одного закрывает остальные. Браузер, который атрибут не знает, оставит блоки независимыми — ничего не сломается, просто открытыми могут оказаться несколько ответов сразу.

popover делает элемент всплывающим: он спрятан, пока его не откроет кнопка с атрибутом popovertarget, в котором записан id элемента, и закрывается щелчком мимо или клавишей Esc. В браузере, который атрибут не знает, кнопка ничего не сделает, а сам элемент будет виден на странице всегда.

Команды кнопок (invoker commands) заменяют и вызов showModal(): <button commandfor="confirm" command="show-modal"> открывает окно с id confirm модальным — с Esc и отключённой страницей — без строки скрипта, а command="close" его закрывает. Но в браузере постарше такая кнопка молча не сработает, и окно не откроется вовсе. Поэтому, пока статус Newly, важное окно открывают showModal().

Как плавно проявить содержимое открытого <details> и поповер — тоже без скрипта, одним стилем, — покажет вторая глава, в статье про переходы.

  • Самодельный вопрос с ответом скрипт должен учить всему: щелчку, фокусу, клавишам и сообщению для программы чтения. <details> и <summary> дают это сами, Baseline Widely available с 15 июля 2022 года.
  • Щелчок, Enter и Пробел на <summary> ставят и снимают атрибут open. Закрытый ответ сохраняет свои размеры и стиль, поэтому открытость проверяют у самого <details>: по высоте или свойству open.
  • <dialog> без open спрятан через display: none. Форма с method="dialog" закрывает окно без скрипта и пишет value нажатой кнопки в returnValue. Baseline Widely available с 14 сентября 2024 года.
  • Esc, фокус внутри окна и отключённая страница — свойства модального окна, а его открывает showModal(), то есть скрипт.
  • inert отгораживает целую область от фокуса, щелчков и программы чтения, а disabled выключает только поля формы. От скрипта inert не защищает. Baseline Widely available с 11 октября 2025 года.
  • name у <details>, popover и команды кнопок — Baseline Newly available: знайте о них, но не стройте на них единственный путь к важному.

1/4Два блока <details class="faq">: у summary высота 30 пикселей, у абзаца ответа — 40, первый блок закрыт. Что напечатает measure(".faq p", "height") для абзаца закрытого блока?

2/4В окне <dialog open> форма <form method="dialog"> с кнопкой <button value="later">Позже</button>. Что будет после щелчка по кнопке?

3/4На время оплаты из очереди Tab нужно убрать кнопку «Оплатить» и ссылку «Условия», которые стоят в одном блоке. Что для этого подходит?

4/4Окно <dialog open> не закрывается по Esc. Почему?

Вопросы, которые не раскрываются

На странице «Вопросы и ответы» вопросы набраны заголовками <h3>, ответы — абзацами, и все ответы видны сразу. Переделайте разметку без скрипта: ответ на «Доставка?» закрыт и раскрывается по щелчку, ответ на «Оплата?» открыт с самого начала, оба вопроса принимают фокус.

Классы оставьте: проверка ищет блоки по классу faq, а вопросы — по классу q. Когда всё получится, прибор напечатает .faq → height: 30px, 70px и .q → фокус: да, да.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Каждый блок становится <details class="faq">, заголовок — <summary class="q">. Второму блоку нужен атрибут open, первому — ничего: закрыт он по умолчанию.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вывод: .faq → height: 30px, 70px и .q → фокус: да, да. Щелчок по «Доставка?» в рамке раскроет первый ответ, а Enter и Пробел на вопросе с фокусом делают то же самое.

Окно, которое не закрывается

Окно спрашивает «Удалить черновик?», но щелчок по «Оставить» отправляет форму, а окно остаётся на месте. Сделайте так, чтобы обе кнопки закрывали окно без скрипта, а по returnValue было видно, какую нажали.

Квадратные скобки в адресе проверяют атрибут: dialog[open] — открытые окна. Нужный вывод — одна строка dialog[open] → 0. Какая кнопка нажата, проверьте, дописав в конец measure("dialog", "returnValue"): получиться должно keep.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Форме не хватает method="dialog". Без него она отправляется как обычная форма — прибор перехватил отправку и напечатал «форма отправлена», — а окно никто не закрывает. Значения у кнопок уже есть, их и запишет returnValue.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вывод: dialog[open] → 0 и dialog → returnValue: keep. Щелчок по «Удалить» дал бы delete.

Невидимое меню ловит Tab

Закрытое меню сайта спрятано прозрачностью: opacity: 0 делает элемент полностью прозрачным, но оставляет на месте. Глазу меню не видно, а Tab по-прежнему заходит в его ссылки, и человек с клавиатурой теряет фокус в пустоте. Добавьте один атрибут, чтобы ссылки закрытого меню выпали из фокуса, а кнопка «Купить» осталась. Нужный вывод: a, button → фокус: нет, нет, да.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Меню нужен inert: пока оно закрыто, до его ссылок не доходят ни Tab, ни щелчки, ни программа чтения.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вывод: a, button → фокус: нет, нет, да. Когда меню откроют, скрипт снимет и прозрачность, и inert.