12k
All articles

Управление фокусом и интерактивностью с помощью атрибута Inert

Используйте атрибут inert, чтобы изолировать модальные окна, панели и оверлеи загрузки, блокируя фокус, клики и доступ к дереву доступности.

OpenReplay Team
OpenReplay Team
Управление фокусом и интерактивностью с помощью атрибута Inert

Установка inert на элементе полностью исключает его поддерево из порядка обхода по Tab, блокирует все события указателя и клика и скрывает его из дерева доступности, чтобы программы чтения с экрана не могли его обнаружить или озвучить — эти три поведения закреплены спецификацией WHATWG HTML Living Standard. Кроме того, современные браузеры запрещают внутристраничному поиску (Ctrl/Cmd+F) находить текст в поддереве и отключают выделение текста внутри него — поведения, которые спецификация оставляет на усмотрение пользовательского агента, но которые MDN документирует как реализованную норму. Итого — шесть каналов взаимодействия отключаются одним атрибутом.

Эта статья решает конкретную задачу: как корректно отключить фоновый контент при открытии модального окна, выдвижной панели или бокового меню — без ручной реализации ловушки фокуса, которая ломается на мобильных программах чтения с экрана, и без разбрасывания aria-hidden по всему DOM. Рассматривается, что именно блокирует inert, синтаксис HTML и JavaScript для его применения, полноценный пример модального окна с восстановлением фокуса, способы стилизации инертного контента и случаи, когда вместо него лучше использовать disabled, aria-hidden или hidden.

Ключевые выводы

  • inert блокирует шесть каналов взаимодействия одним объявлением: фокус, события указателя и клика, порядок обхода по Tab и обнаруживаемость в дереве доступности (всё это закреплено спецификацией), а также внутристраничный поиск и выделение текста (на усмотрение UA, но реализовано во всех современных браузерах).
  • inert получил статус Baseline Newly available в апреле 2023 года — Chrome и Edge 102, Firefox 112, Safari 15.5 — и достиг статуса Baseline Widely available приблизительно в октябре 2025 года, что переводит полифил wicg-inert в разряд исторического контекста, а не производственного требования.
  • Смена ментальной модели: охрана вместо ловушки. Ловушка фокуса удерживает пользователей внутри компонента с помощью JavaScript; inert охраняет остальную часть страницы, и браузер сам обеспечивает соблюдение границы.
  • Помимо булева HTML-атрибута, inert доступен как IDL-свойство HTMLElement.inert — булево значение, устанавливаемое в JavaScript: mainEl.inert = true при открытии, mainEl.inert = false при закрытии.
  • <dialog>.showModal() автоматически делает остальную часть страницы инертной, поэтому ручное управление inert необходимо только для пользовательских диалоговых паттернов, реализованных без нативного элемента.

Что блокирует атрибут inert

inert — это глобальный HTML-атрибут, который делает элемент и всё его поддерево неинтерактивным и недоступным для обнаружения. Согласно разделу об инертных поддеревьях спецификации WHATWG HTML Living Standard, инертный узел не получает целевых пользовательских событий взаимодействия, таких как click и focus, а пользовательские агенты обязаны исключать его из дерева доступности. Три блокировки являются нормативными и единообразно реализованы во всех браузерах:

  1. Фокус — инертные элементы не могут получить фокус ни по Tab, ни по клику, ни программно через element.focus().
  2. События указателя и клика — инициированные пользователем клики и события указателя не достигают инертных узлов.
  3. Обнаруживаемость в дереве доступности — вспомогательные технологии не могут найти или озвучить поддерево.

Ещё две блокировки спецификация оставляет на усмотрение пользовательского агента (используется нормативное may), однако MDN документирует их как реализованное поведение во всех современных браузерах:

  1. Внутристраничный поиск — Ctrl/Cmd+F не находит текст внутри инертного поддерева.
  2. Выделение текста — пользователи не могут выделять инертный текст.

Исключение из порядка обхода по Tab является следствием блокировки фокуса: поскольку инертные узлы не могут получить фокус, они полностью исключаются из последовательной навигации. Важная граница: inert блокирует инициированные пользователем события, но не программные. Вызов dispatchEvent() или срабатывание таймера внутри инертного поддерева по-прежнему выполняются — inert не является аналогом alert() и не замораживает выполнение JavaScript.

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

Два канонических сценария использования

inert предназначен для двух ситуаций, обе задокументированы в руководстве по inert на web.dev: DOM, который присутствует в разметке, но находится за пределами экрана или скрыт, и DOM, который видим, но не должен быть интерактивным.

DOM за пределами экрана или скрытый DOM. Выдвижная навигационная панель или боковое меню добавляет в DOM фокусируемые ссылки ещё до того, как они становятся видимыми. Без inert пользователи клавиатуры могут перейти по Tab в закрытую панель и оказаться на элементах управления, которые не видны. Пометка контейнера панели как инертного до её открытия исключает эти ссылки из порядка обхода:

<nav id="drawer" inert>
  <a href="/dashboard">Dashboard</a>
  <a href="/settings">Settings</a>
</nav>

Видимый, но неинтерактивный UI. Когда форма отправляется, страница загружается или модальный оверлей перекрывает фоновый контент, этот контент визуально присутствует, но не должен принимать ввод. Применение inert к форме во время отправки предотвращает повторные отправки и случайное перемещение фокуса:

<form id="signup" inert>
  <!-- поля отключены как группа, пока запрос выполняется -->
</form>

Оба случая подчиняются одной логике: контент остаётся в DOM (чтобы сохранялись макет, переходы и состояние), но браузер отказывается направлять к нему взаимодействие.

Синтаксис: HTML-атрибут и свойство HTMLElement.inert

inert имеет два интерфейса: булев HTML-атрибут и IDL-свойство HTMLElement.inert. Атрибут предназначен для статической или серверно-рендеримой разметки; свойство — для переключения состояния в JavaScript.

Как булев атрибут, значение имеет само его присутствие — inert и inert="" эквивалентны, а значение по умолчанию — false (отсутствие атрибута означает интерактивность):

<main inert>
  <!-- всё здесь неинтерактивно -->
</main>

Для переключения во время выполнения используйте свойство HTMLElement.inert — булево значение, которое можно читать и устанавливать напрямую, без необходимости использовать setAttribute / removeAttribute:

const mainEl = document.querySelector('main');

// отключить взаимодействие с остальной частью страницы
mainEl.inert = true;

// восстановить
mainEl.inert = false;

Это самая элегантная часть API, которую упускают большинство существующих материалов: переключение при открытии/закрытии — это два присваивания. Сравните с восьмишаговой процедурой ловушки фокуса, описанной ниже.

До inert: ловушки фокуса и их хрупкость

До появления inert стандартным способом ограничить область модального окна была JavaScript-ловушка фокуса — логика, перехватывающая Tab и Shift+Tab для циклической навигации внутри диалога. Каноническая процедура, описанная на CSS-Tricks, занимает около восьми шагов: найти все фокусируемые элементы на странице, определить первый и последний фокусируемый элемент внутри модального окна, убрать интерактивность и обнаруживаемость у всего, что находится за его пределами, переместить фокус внутрь, прослушивать события закрытия, восстановить всё при закрытии и вернуть фокус на триггер.

Первый шаг — «найти все фокусируемые элементы» — сам по себе является источником ошибок, поскольку набор нативно фокусируемых элементов шире, чем большинство разработчиков помнит. Элементы, получающие фокус при последовательной навигации по Tab без какого-либо tabindex:

  • <a> и <area> с атрибутом href
  • <button>, <input>, <select> и <textarea> (если не disabled)
  • <iframe>, <embed> и <object>
  • <audio> и <video> с атрибутом controls
  • <summary> (первый внутри <details>)
  • любой элемент с неотрицательным tabindex и любой элемент с contenteditable

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

Смена ментальной модели — вот в чём суть. Ловушка фокуса удерживает пользователей внутри компонента, перехватывая нажатия клавиш; inert охраняет остальную часть страницы, делая всё за пределами диалога недостижимым — браузер сам обеспечивает соблюдение границы, а не ваш JavaScript. Эта концепция «охрана вместо ловушки» взята из материала LogRocket об этом атрибуте.

Ручные ловушки дают сбой в трёх типичных ситуациях:

  • Мобильные вспомогательные технологии. TalkBack на Android и VoiceOver на iOS используют жесты смахивания, а не нажатия Tab. JavaScript-ловушка, перехватывающая только клавиатурные события, не создаёт никакой границы для пользователей программ чтения с экрана, работающих с жестами. inert блокирует поддерево на уровне платформы, охватывая как клавиатурную, так и жестовую навигацию.
  • Разрастание aria-hidden. До появления inert обходным решением была установка aria-hidden="true" на каждом немодальном элементе. На страницах с глубокими деревьями DOM это становилось неуправляемым и нередко неполным.
  • Ручные циклы Tab. Логика перехвата Tab/Shift+Tab хрупка и легко реализуется с ошибками, особенно когда фокусируемое содержимое модального окна меняется.

Записи сессий для реализаций модальных окон и выдвижных панелей нередко выявляют события фокуса, попадающие на фоновый контент, пока диалог ещё открыт, — характерный признак неполной границы фокуса, и именно то, для устранения чего предназначен inert.

Упомянутые выше источники содержат полную реализацию trap-focus.js; повторять её здесь нет необходимости. Показательно сравнение объёма кода. Ловушка — это десятки строк перехвата событий. Эквивалент на inert выглядит так:

function openModal() {
  mainEl.inert = true;
}
function closeModal() {
  mainEl.inert = false;
}

Полноценный пример модального окна с восстановлением фокуса

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

<button id="open-modal" type="button">Сохранить изменения…</button>

<div
  id="modal"
  class="modal"
  role="dialog"
  aria-labelledby="modal-title"
  aria-modal="true"
  hidden
>
  <h2 id="modal-title">Сохранить изменения?</h2>
  <p>Несохранённые изменения будут потеряны.</p>
  <button id="save" type="button" autofocus>Сохранить</button>
  <button id="cancel" type="button">Отменить</button>
</div>

<main id="page">
  <!-- весь контент страницы -->
</main>
const triggerEl = document.getElementById('open-modal');
const modalEl = document.getElementById('modal');
const mainEl = document.getElementById('page');
const cancelEl = document.getElementById('cancel');

let lastFocused = null;

function openModal() {
  lastFocused = document.activeElement;   // запомнить триггер
  modalEl.hidden = false;
  mainEl.inert = true;                    // заблокировать остальную часть страницы
  // переместить фокус на основное действие диалога
  modalEl.querySelector('[autofocus]').focus();
}

function closeModal() {
  mainEl.inert = false;                   // восстановить страницу
  modalEl.hidden = true;
  if (lastFocused) lastFocused.focus();   // вернуть фокус на триггер
}

triggerEl.addEventListener('click', openModal);
cancelEl.addEventListener('click', closeModal);
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && !modalEl.hidden) closeModal();
});

Несколько замечаний о корректности реализации. Диалог неявно использует семантику tabindex="-1" через свои фокусируемые дочерние элементы; как правило, положительный tabindex нигде не нужен — положительные целые числа переопределяют естественный порядок Tab и являются задокументированным антипаттерном. Используйте tabindex="-1" только тогда, когда нужно программно сфокусировать неинтерактивный контейнер, и tabindex="0" — только для действительно интерактивных пользовательских элементов. Атрибут autofocus на основном действии является рекомендованной спецификацией начальной точкой для фокуса внутри диалога. Этот паттерн работает в Chrome 102+, Firefox 112+ и Safari 15.5+.

Стилизация инертного контента: стилей по умолчанию нет

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

[inert],
[inert] * {
  opacity: 0.5;         /* визуальное затемнение — сигнализирует о неактивности */
  pointer-events: none; /* убирает эффекты наведения и изменения курсора */
  user-select: none;    /* предотвращает выделение текста */
  cursor: default;
}

Каждое свойство обосновано: opacity визуально сообщает о неактивном состоянии, pointer-events: none убирает состояния наведения и изменения курсора, которые иначе подразумевали бы интерактивность, а user-select: none соответствует блокировке выделения текста, уже применяемой атрибутом. Поведение обеспечивается самим inert; CSS существует для того, чтобы зрячие пользователи могли видеть границу, которую браузер обеспечивает «под капотом».

Выбор между inert, disabled, aria-hidden, hidden и pointer-events

Выбирайте инструмент исходя из области применения и поведения в дереве доступности: inert блокирует все каналы взаимодействия во всём поддереве и удаляет его из дерева доступности; disabled блокирует отдельный элемент управления, но оставляет его обнаруживаемым; aria-hidden скрывает контент от вспомогательных технологий, оставляя клики и фокус работающими; hidden полностью удаляет контент; а CSS pointer-events: none блокирует только мышь. Используйте inert, когда нужно полностью изолировать фоновый контент за модальным окном, выдвижной панелью или оверлеем загрузки.

ИнструментБлокирует взаимодействиеВ дереве доступности?Область примененияКогда использовать
inertДа (фокус, указатель, поиск, выделение)Нет — удалёнЭлемент + поддеревоИзоляция фонового контента за модальным окном, выдвижной панелью или оверлеем загрузки
disabledДа (для элемента управления)Да — объявляется как недоступныйЭлемент управления формы или группа fieldsetОтдельная кнопка, поле ввода или раздел формы, временно недоступные для действий
aria-hidden="true"Нет — клики и фокус работаютНет — удалёнЭлемент + поддеревоСкрытие декоративного или дублирующего контента только от вспомогательных технологий
hidden / display:noneДа — полностью удалёнНет — не отрисовываетсяЭлемент + поддеревоКонтент, который прямо сейчас не должен существовать ни визуально, ни для вспомогательных технологий
pointer-events: noneТолько мышь — клавиатура и вспомогательные технологии не затронутыДаЭлемент + поддеревоКосметическое «прохождение» кликов; никогда не является заменой inert

Две распространённые ошибки: использование aria-hidden для фонового контента при сохранении его кликабельности и фокусируемости (он остаётся в порядке Tab), и использование pointer-events: none в расчёте на то, что пользователи клавиатуры и программ чтения с экрана тоже заблокированы (это не так). Для полной изоляции фона inert — единственный инструмент, охватывающий все каналы.

Нужен ли inert при использовании dialog.showModal()?

При открытии диалога через HTMLDialogElement.showModal() браузер автоматически делает остальную часть страницы инертной — поведение верхнего слоя включает неявную инертную границу, поэтому всё за пределами диалога становится недоступным для кликов и Tab без какого-либо управления атрибутами с вашей стороны. Ручное управление inert необходимо только при создании пользовательского диалогового паттерна без нативного элемента <dialog>, как в примере выше.

<dialog id="confirm">
  <p>Удалить этот элемент?</p>
  <button>Удалить</button>
  <button>Отмена</button>
</dialog>
document.getElementById('confirm').showModal(); // страница автоматически становится инертной

Если вы можете использовать <dialog> с showModal(), инертная граница предоставляется бесплатно. Прибегайте к ручному управлению inert, когда проблемы с поддержкой вспомогательных технологий или дизайнерские ограничения вынуждают вас реализовывать пользовательский диалог.

Поддержка браузерами и новое CSS-свойство interactivity

inert получил статус Baseline Newly available в апреле 2023 года — Chrome и Edge реализовали его в версии 102, Firefox — в 112, Safari — в 15.5 — и достиг статуса Baseline Widely available приблизительно в октябре 2025 года (через 30 месяцев после даты совместимости). Полифил wicg-inert теперь является историческим контекстом, а не производственным требованием; его последний релиз — v3.1.3 (2023), и он больше не поддерживается активно. В собственном README полифила отмечается, что он «ресурсозатратен с точки зрения производительности», поскольку «требует значительного обхода дерева» — затрат, которых нативная реализация избегает. Для любого браузера, выпущенного после 2023 года, он не нужен.

Более новой CSS-альтернативой является свойство interactivity, которое принимает значение interactivity: inert для применения инертного поведения через таблицу стилей, а не через атрибут. Это развивающаяся возможность с ограниченной поддержкой: согласно данным caniuse, она доступна только в Chromium (Chrome/Edge 135+, март 2025) без поддержки Firefox и Safari по состоянию на середину 2026 года и не имеет статуса Baseline (Limited availability). Рассматривайте её как перспективный вариант для контекстов, ориентированных только на Chromium, но не как кроссбраузерную замену атрибута.

Заключение

Для изоляции фонового контента за модальным окном, выдвижной панелью или оверлеем загрузки inert заменяет весь хрупкий аппарат ручных ловушек фокуса и разрастающегося aria-hidden одним атрибутом, соблюдение которого браузер обеспечивает для клавиатурной, указательной и навигации с помощью вспомогательных технологий. Проверьте существующие диалоги: если пользователь клавиатуры может перейти по Tab из открытого модального окна на страницу за ним, оберните этот фоновый контент — или ваш <main> — в inert, переключайте его через element.inert при открытии и закрытии и восстанавливайте фокус на триггере. Поскольку атрибут получил статус Widely available с 2025 года, единственный оставшийся вопрос — предоставляет ли нативный <dialog> инертную границу бесплатно.

Часто задаваемые вопросы

В чём разница между атрибутом inert и aria-hidden?

Атрибут inert блокирует взаимодействие и удаляет контент из дерева доступности, делая поддерево одновременно недостижимым и необнаруживаемым. Атрибут aria-hidden только удаляет контент из дерева доступности, не блокируя клики, фокус или клавиатурное взаимодействие. Применение aria-hidden к фоновому контенту при сохранении его кликабельности и фокусируемости — распространённая ошибка, поскольку такие элементы остаются в порядке Tab. Используйте inert, когда нужно заблокировать взаимодействие во всём поддереве.

Блокирует ли inert обработчики событий JavaScript и программные события?

Нет. Атрибут inert блокирует инициированные пользователем события, такие как клики, фокус и события указателя, но не останавливает программные события. Вызов dispatchEvent, callback таймера или любой скрипт, выполняемый внутри инертного поддерева, продолжает работать в штатном режиме. В отличие от функции alert, inert не замораживает выполнение JavaScript; он лишь изменяет то, как браузер направляет пользовательское взаимодействие и обнаружение вспомогательными технологиями к помеченному поддереву.

Нужен ли JavaScript-полифил для inert?

Нет, для любого браузера, выпущенного после 2023 года. Атрибут inert получил статус Baseline Newly available в апреле 2023 года — Chrome и Edge 102, Firefox 112, Safari 15.5 — и достиг статуса Baseline Widely available приблизительно в октябре 2025 года. Полифил wicg-inert теперь является историческим контекстом, а не производственным требованием; его последний релиз — v3.1.3 в 2023 году, и он больше не поддерживается активно. В README также отмечается, что полифил ресурсозатратен с точки зрения производительности, поскольку требует обхода дерева, которого нативные реализации избегают.

Почему традиционные ловушки фокуса не работают на мобильных программах чтения с экрана?

Традиционные ловушки фокуса не работают на мобильных устройствах, потому что TalkBack на Android и VoiceOver на iOS используют жесты смахивания, а не нажатия Tab. JavaScript-ловушка, перехватывающая только клавиатурные события, не создаёт никакой границы для пользователей программ чтения с экрана, работающих с жестами, — они могут выйти из диалога на фоновый контент. Атрибут inert блокирует поддерево на уровне платформы, охватывая как клавиатурную навигацию, так и навигацию с помощью жестов, — именно поэтому он заменяет ручную логику ловушки фокуса при ограничении области модальных окон.

Digital experience platform

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.