12k
All articles

Gerenciando Foco e Interatividade com o Atributo Inert

Use o atributo inert para isolar modais, drawers e overlays de carregamento, bloqueando foco, cliques e acesso à árvore de acessibilidade.

OpenReplay Team
OpenReplay Team
Gerenciando Foco e Interatividade com o Atributo Inert

Definir inert em um elemento remove toda a sua subárvore da ordem de tabulação, bloqueia todos os eventos de ponteiro e clique, e o oculta da árvore de acessibilidade, impedindo que leitores de tela o descubram ou anunciem — esses três comportamentos são obrigatórios pela especificação do WHATWG HTML Living Standard. Além disso, os navegadores atuais impedem que a busca na página (Ctrl/Cmd+F) corresponda ao texto da subárvore e desabilitam a seleção de texto dentro dela — comportamentos que a especificação deixa a critério do agente de usuário, mas que o MDN documenta como o padrão implementado. São seis canais de interação desabilitados com um único atributo.

Este artigo resolve um problema específico: como desabilitar de forma limpa o conteúdo em segundo plano quando um modal, drawer ou menu lateral é aberto — sem precisar implementar manualmente uma armadilha de foco que quebra em leitores de tela mobile ou espalhar aria-hidden por todo o DOM. O artigo aborda o que o inert bloqueia, a sintaxe HTML e JavaScript para aplicá-lo, um modal completo com restauração de foco, como estilizar conteúdo inerte e quando usar disabled, aria-hidden ou hidden em vez dele.

Principais Conclusões

  • O inert bloqueia seis canais de interação em uma única declaração: foco, eventos de ponteiro/clique, ordem de tabulação e descoberta na árvore de acessibilidade (todos obrigatórios pela especificação), além de busca na página e seleção de texto (a critério do agente de usuário, mas implementados nos navegadores atuais).
  • O inert tornou-se Baseline Newly available em abril de 2023 — Chrome e Edge 102, Firefox 112, Safari 15.5 — e atingiu o status Baseline Widely available por volta de outubro de 2025, tornando o polyfill wicg-inert um contexto legado em vez de um requisito de produção.
  • A mudança de modelo mental é de guarda vs. armadilha: uma armadilha de foco prende os usuários dentro de um componente com JavaScript; o inert protege o restante da página para que o navegador imponha o limite nativamente.
  • Além do atributo booleano HTML, o inert é exposto como a propriedade IDL HTMLElement.inert, um booleano que você define em JavaScript — mainEl.inert = true ao abrir, mainEl.inert = false ao fechar.
  • O <dialog>.showModal() automaticamente aplica inert ao restante da página, portanto o gerenciamento manual do inert só é necessário para padrões de diálogo personalizados construídos fora do elemento nativo.

O que o Atributo inert Bloqueia

O inert é um atributo HTML global que torna um elemento e toda a sua subárvore não interativos e não descobríveis. De acordo com a seção de subárvores inertes do WHATWG HTML Living Standard, um nó inerte não recebe eventos de interação do usuário direcionados, como clique e foco, e os agentes de usuário devem excluí-lo da árvore de acessibilidade. Três bloqueios são normativos e consistentes em todas as implementações:

  1. Foco — elementos inertes não podem receber foco por Tab, clique ou element.focus() programático.
  2. Eventos de ponteiro e clique — cliques e eventos de ponteiro iniciados pelo usuário não chegam a nós inertes.
  3. Descoberta na árvore de acessibilidade — tecnologias assistivas não conseguem encontrar ou anunciar a subárvore.

Dois bloqueios adicionais são deixados a critério do agente de usuário na especificação (ela usa o normativo may), mas o MDN os documenta como o comportamento implementado nos navegadores atuais:

  1. Busca na página — Ctrl/Cmd+F não encontra texto dentro de uma subárvore inerte.
  2. Seleção de texto — os usuários não conseguem selecionar texto inerte.

A ordem de tabulação decorre do bloqueio de foco: como nós inertes não podem receber foco, eles são removidos completamente da navegação sequencial por foco. Um limite importante: o inert bloqueia eventos iniciados pelo usuário, não os programáticos. Uma chamada dispatchEvent() ou um timer disparado dentro de uma subárvore inerte ainda é executado — o inert não é alert() e não congela a execução do JavaScript.

A armadilha a ser internalizada: como o inert remove a subárvore da árvore de acessibilidade, nunca o aplique a conteúdo que o usuário ainda precisa ler. Se você só precisa ocultar algo visualmente mantendo-o descobrível, essa é uma ferramenta diferente.

Os Dois Casos de Uso Canônicos

O inert existe para duas situações, ambas documentadas no guia inert do web.dev: DOM que está presente mas fora da tela ou oculto, e DOM que está visível mas não deve ser interativo.

DOM fora da tela ou oculto. Um drawer de navegação deslizante ou menu lateral adiciona links focalizáveis ao DOM antes de estarem visíveis. Sem o inert, usuários de teclado podem pressionar Tab para entrar no drawer fechado e chegar a controles que não conseguem ver. Marcar o contêiner do drawer como inerte até que ele seja aberto mantém esses links fora da ordem de tabulação:

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

Interface visível mas não interativa. Quando um formulário está sendo enviado, uma página está carregando ou uma sobreposição modal está sobre o conteúdo em segundo plano, esse conteúdo está visivelmente presente mas não deve aceitar entrada. Aplicar inert ao formulário durante o envio evita envios duplicados e foco indesejado:

<form id="signup" inert>
  <!-- campos desabilitados em grupo enquanto a requisição está em andamento -->
</form>

Ambos os casos compartilham a mesma lógica: o conteúdo permanece no DOM (para que layout, transições e estado sobrevivam), mas o navegador recusa-se a rotear interações para ele.

Sintaxe: O Atributo HTML e a Propriedade HTMLElement.inert

O inert possui duas interfaces: o atributo booleano HTML e a propriedade IDL HTMLElement.inert. O atributo é para marcação estática ou renderizada no servidor; a propriedade é para alternar o estado em JavaScript.

Como atributo booleano, o que importa é sua presença — inert e inert="" são equivalentes, e o valor padrão é false (ausente significa interativo):

<main inert>
  <!-- tudo aqui é não interativo -->
</main>

Para alterná-lo em tempo de execução, use a propriedade HTMLElement.inert, um booleano que você pode ler e definir diretamente — sem necessidade de usar setAttribute / removeAttribute:

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

// desabilitar interação com o restante da página
mainEl.inert = true;

// restaurar
mainEl.inert = false;

Esta é a parte mais limpa da API e a parte que falta na maioria dos artigos existentes: o toggle de abrir/fechar são duas atribuições. Compare isso com o procedimento de oito etapas de armadilha de foco descrito abaixo.

Antes do inert: Armadilhas de Foco e Por Que Eram Frágeis

Antes do inert, a forma padrão de delimitar um modal era uma armadilha de foco em JavaScript — lógica que intercepta Tab e Shift+Tab para manter o foco dentro do diálogo. O procedimento canônico, enumerado pelo CSS-Tricks, chega a aproximadamente oito etapas: encontrar todos os elementos focalizáveis na página, identificar o primeiro e o último elemento focalizável dentro do modal, remover interatividade e descoberta de tudo fora dele, mover o foco para dentro, escutar eventos de fechamento, restaurar tudo ao fechar e retornar o foco ao elemento que disparou a abertura.

Essa primeira etapa — “encontrar todos os elementos focalizáveis” — é em si uma fonte de bugs, porque o conjunto de elementos nativamente focalizáveis é maior do que a maioria das pessoas lembra. Os elementos que recebem foco na ordem sequencial de tabulação sem nenhum tabindex são:

  • <a> e <area> com um href
  • <button>, <input>, <select> e <textarea> (a menos que disabled)
  • <iframe>, <embed> e <object>
  • <audio> e <video> com um atributo controls
  • <summary> (o primeiro dentro de um <details>)
  • qualquer elemento com um tabindex não negativo e qualquer elemento contenteditable

Perder um ao construir uma armadilha permite que um usuário de teclado escape dela; gerenciar demais a lista e você briga com a própria ordenação do navegador. O padrão mais seguro é deixar a ordem de foco natural do documento intacta e intervir apenas quando um componente genuinamente exige isso — que é a maior parte do trabalho manual que o inert elimina.

A mudança de modelo mental é o ponto central. Uma armadilha de foco prende os usuários dentro de um componente interceptando pressionamentos de tecla; o inert protege o restante da página tornando tudo fora do diálogo inacessível — o navegador impõe o limite, não o seu JavaScript. Esse enquadramento de guarda vs. armadilha vem do tratamento do atributo pelo LogRocket.

Armadilhas feitas manualmente falham de três maneiras recorrentes:

  • Tecnologia assistiva mobile. O TalkBack no Android e o VoiceOver no iOS navegam por gestos de deslizamento, não por pressionamentos de Tab. Uma armadilha JavaScript que só intercepta eventos de teclado não fornece nenhum limite para usuários de leitores de tela baseados em gestos. O inert bloqueia a subárvore no nível da plataforma, cobrindo tanto a navegação por teclado quanto por gestos.
  • Proliferação de aria-hidden. A solução alternativa anterior ao inert era definir aria-hidden="true" em cada elemento não modal. Em uma página com árvores DOM profundas, isso se torna impossível de manter e frequentemente incompleto.
  • Loops de tabulação manuais. A lógica de interceptação de Tab/Shift+Tab é frágil e fácil de errar, especialmente quando o conteúdo focalizável do modal muda.

Replays de sessão de implementações de modal e drawer frequentemente revelam eventos de foco chegando ao conteúdo em segundo plano enquanto um diálogo ainda está aberto — a assinatura de um limite de foco incompleto, e precisamente o que o inert foi projetado para eliminar.

As referências acima reconstroem um trap-focus.js completo; não há necessidade de repetir isso aqui. A comparação relevante é a contagem de linhas. A armadilha são dezenas de linhas de interceptação de eventos. O equivalente com inert é este:

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

Exemplo Completo de Modal com Restauração de Foco

O padrão de modal personalizado mais limpo posiciona o diálogo como irmão de <main inert>: o modal fica fora da subárvore inerte, portanto permanece interativo enquanto tudo em <main> está bloqueado. Esse padrão de irmão <main inert> segue a estrutura documentada pelo CSS-Tricks. O exemplo abaixo adiciona a parte que toda referência ignora — mover o foco para dentro do diálogo ao abrir e restaurá-lo ao elemento de gatilho ao fechar.

<button id="open-modal" type="button">Salvar alterações…</button>

<div
  id="modal"
  class="modal"
  role="dialog"
  aria-labelledby="modal-title"
  aria-modal="true"
  hidden
>
  <h2 id="modal-title">Salvar alterações?</h2>
  <p>Suas alterações não salvas serão perdidas.</p>
  <button id="save" type="button" autofocus>Salvar</button>
  <button id="cancel" type="button">Descartar</button>
</div>

<main id="page">
  <!-- todo o conteúdo da página -->
</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;   // lembrar o elemento de gatilho
  modalEl.hidden = false;
  mainEl.inert = true;                    // proteger o restante da página
  // mover o foco para a ação principal do diálogo
  modalEl.querySelector('[autofocus]').focus();
}

function closeModal() {
  mainEl.inert = false;                   // restaurar a página
  modalEl.hidden = true;
  if (lastFocused) lastFocused.focus();   // restaurar o foco ao elemento de gatilho
}

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

Algumas notas sobre correção. O diálogo usa a semântica de tabindex="-1" implicitamente por meio de seus filhos focalizáveis; geralmente você não precisa de um tabindex positivo em lugar algum — inteiros positivos substituem a ordem de tabulação natural e são um anti-padrão documentado. Use tabindex="-1" apenas quando precisar focar um contêiner não interativo programaticamente, e tabindex="0" apenas para elementos personalizados genuinamente interativos. O atributo autofocus na ação principal é o ponto de partida de foco recomendado pela especificação dentro de um diálogo. Esse padrão funciona no Chrome 102+, Firefox 112+ e Safari 15.5+.

Estilizando Conteúdo Inerte: Não Há Padrão Visual

O inert não tem efeito visual padrão — o navegador muda o comportamento, não a aparência, portanto o conteúdo inerte parece idêntico ao conteúdo ativo a menos que você o estilize. O padrão comum, mostrado no web.dev, usa o seletor de atributo [inert] e combina três propriedades que espelham os canais de interação que o atributo bloqueia:

[inert],
[inert] * {
  opacity: 0.5;         /* escurecimento visual — sinaliza "não ativo" */
  pointer-events: none; /* suprime affordances de hover/cursor */
  user-select: none;    /* impede seleção de texto */
  cursor: default;
}

Cada propriedade tem seu papel: opacity comunica visualmente o estado desabilitado, pointer-events: none remove estados de hover e mudanças de cursor que de outra forma implicariam interatividade, e user-select: none corresponde ao bloqueio de seleção de texto que o atributo já aplica. O comportamento é imposto pelo próprio inert; o CSS existe para que usuários com visão possam ver o limite que o navegador está impondo nos bastidores.

Escolhendo Entre inert, disabled, aria-hidden, hidden e pointer-events

Escolha a ferramenta pelo escopo e pelo comportamento na árvore de acessibilidade: o inert bloqueia todos os canais de interação em uma subárvore inteira e a remove da árvore de acessibilidade; o disabled bloqueia um único controle mas o mantém descobrível; o aria-hidden oculta conteúdo de tecnologias assistivas enquanto mantém cliques e foco intactos; o hidden remove o conteúdo completamente; e o CSS pointer-events: none bloqueia apenas o mouse. Use inert sempre que precisar bloquear o conteúdo em segundo plano atrás de um modal, drawer ou sobreposição de carregamento.

FerramentaBloqueia interaçãoNa árvore de acessibilidade?EscopoUse quando
inertSim (foco, ponteiro, busca, seleção)Não — removidoElemento + subárvoreBloquear conteúdo em segundo plano atrás de um modal, drawer ou sobreposição de carregamento
disabledSim (para o controle)Sim — anunciado como indisponívelControle de formulário ou grupo fieldsetUm único botão, input ou seção de formulário temporariamente não acionável
aria-hidden="true"Não — cliques/foco ainda funcionamNão — removidoElemento + subárvoreOcultar conteúdo decorativo ou duplicado apenas de tecnologias assistivas
hidden / display:noneSim — completamente removidoNão — não renderizadoElemento + subárvoreConteúdo que não deve existir visualmente ou para tecnologias assistivas no momento
pointer-events: noneApenas mouse — teclado/AT não afetadosSimElemento + subárvoreClick-through cosmético; nunca um substituto para inert

Os dois erros comuns: usar aria-hidden no conteúdo em segundo plano enquanto o deixa clicável e focalizável (ele permanece na ordem de tabulação), e usar pointer-events: none assumindo que usuários de teclado e leitores de tela estão bloqueados (não estão). Para bloqueio completo do segundo plano, o inert é a única ferramenta que cobre todos os canais.

Você Ainda Precisa de inert com dialog.showModal()?

Quando você abre um diálogo com HTMLDialogElement.showModal(), o navegador automaticamente aplica inert ao restante da página — o comportamento de top-layer inclui um limite inerte implícito, portanto tudo fora do diálogo torna-se não clicável e não tabulável sem nenhum gerenciamento de atributo da sua parte. O inert manual só é necessário quando você constrói um padrão de diálogo personalizado fora do elemento nativo <dialog>, como no exemplo trabalhado acima.

<dialog id="confirm">
  <p>Excluir este item?</p>
  <button>Excluir</button>
  <button>Cancelar</button>
</dialog>
document.getElementById('confirm').showModal(); // página automaticamente com inert

Se você pode usar <dialog> com showModal(), você obtém o limite inerte gratuitamente. Recorra ao inert manual quando preocupações com suporte a tecnologias assistivas ou restrições de design levem você a um diálogo personalizado.

Suporte a Navegadores e a Propriedade CSS interactivity Emergente

O inert tornou-se Baseline Newly available em abril de 2023 — Chrome e Edge lançaram na versão 102, Firefox na 112 e Safari na 15.5 — e atingiu o status Baseline Widely available por volta de outubro de 2025 (30 meses após a data interoperável). O polyfill wicg-inert é agora contexto legado em vez de um requisito de produção; seu último lançamento é v3.1.3 (2023) e não está mais sendo mantido ativamente. Seu próprio README observa que o polyfill é “caro, em termos de desempenho” porque “requer uma quantidade considerável de percurso de árvore” — um custo que a implementação nativa evita. Para qualquer navegador lançado desde 2023, você não precisa dele.

Uma alternativa mais recente baseada em CSS é a propriedade interactivity, que aceita interactivity: inert para aplicar o comportamento inerte por meio de uma folha de estilos em vez de um atributo. É um recurso emergente com suporte mais restrito: de acordo com os dados do caniuse, é exclusivo do Chromium (Chrome/Edge 135+, março de 2025), sem suporte no Firefox ou Safari até meados de 2026, e não é Baseline (disponibilidade limitada). Trate-o como uma opção voltada para o futuro em contextos exclusivos do Chromium, não como um substituto multiplataforma para o atributo.

Conclusão

Para bloquear o conteúdo em segundo plano atrás de um modal, drawer ou sobreposição de carregamento, o inert substitui todo o aparato frágil de armadilhas de foco feitas manualmente e proliferação de aria-hidden por um único atributo que o navegador impõe em toda a navegação por teclado, ponteiro e tecnologia assistiva. Audite seus diálogos existentes: se um usuário de teclado consegue pressionar Tab para sair de um modal aberto e acessar a página atrás dele, envolva esse conteúdo em segundo plano — ou seu <main> — com inert, alterne-o com element.inert ao abrir e fechar, e restaure o foco ao elemento de gatilho. Com o atributo amplamente disponível desde 2025, a única decisão restante é se o <dialog> nativo fornece o limite gratuitamente.

Perguntas Frequentes

Qual é a diferença entre o atributo inert e aria-hidden?

O atributo inert bloqueia a interação e remove o conteúdo da árvore de acessibilidade, tornando a subárvore tanto inacessível quanto não descobrível. O atributo aria-hidden apenas remove o conteúdo da árvore de acessibilidade; ele não bloqueia cliques, foco ou interação por teclado. Aplicar aria-hidden ao conteúdo em segundo plano enquanto o deixa clicável e focalizável é um erro comum, pois esses elementos permanecem na ordem de tabulação. Use inert quando precisar bloquear a interação em uma subárvore inteira.

O inert bloqueia listeners de eventos JavaScript e eventos programáticos?

Não. O atributo inert bloqueia eventos iniciados pelo usuário, como cliques, foco e interação por ponteiro, mas não interrompe eventos programáticos. Uma chamada dispatchEvent, um callback de timer ou qualquer script executando dentro de uma subárvore inerte ainda é executado normalmente. Ao contrário da função alert, o inert não congela a execução do JavaScript; ele apenas muda como o navegador roteia a interação do usuário e a descoberta de acessibilidade para a subárvore marcada.

Ainda preciso de um polyfill JavaScript para inert?

Não, para qualquer navegador lançado desde 2023. O atributo inert tornou-se Baseline Newly available em abril de 2023, com Chrome e Edge 102, Firefox 112 e Safari 15.5, e atingiu o status Baseline Widely available por volta de outubro de 2025. O polyfill wicg-inert é agora contexto legado em vez de um requisito de produção; seu último lançamento é v3.1.3 em 2023 e não está mais sendo mantido ativamente. Seu README também observa que é caro em termos de desempenho porque requer percurso de árvore que as implementações nativas evitam.

Por que armadilhas de foco tradicionais falham em leitores de tela mobile?

Armadilhas de foco tradicionais falham no mobile porque o TalkBack no Android e o VoiceOver no iOS navegam por gestos de deslizamento em vez de pressionamentos de Tab. Uma armadilha JavaScript que intercepta apenas eventos de teclado não fornece nenhum limite para usuários de leitores de tela baseados em gestos, portanto eles podem escapar do diálogo para o conteúdo em segundo plano. O atributo inert bloqueia a subárvore no nível da plataforma, cobrindo tanto a navegação por teclado quanto por gestos, razão pela qual substitui a lógica de armadilha de foco feita manualmente para delimitar modais.

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.