Alternar tema

Dialog, Sheet e Popover: acessibilidade e gerenciamento de foco em componentes de sobreposição

Easton editorial illustration: step-by-step assembly path

Um cliente enviou um e-mail: “Quando a sobreposição do site abre e pressionamos Tab, o foco vai para a página ao fundo. Quem usa teclado simplesmente não consegue operá-la.”

O e-mail foi bem constrangedor, porque eu tinha acabado de implementar essa sobreposição na semana anterior.

Ao abrir o código, o problema ficou evidente: depois que o Dialog era aberto, o foco continuava no botão da página ao fundo. Naturalmente, ao pressionar Tab, ele seguia pela página de fundo. Para quem usa leitor de tela, a situação era ainda pior: essas pessoas nem sabiam que a sobreposição tinha sido aberta, pois o foco não entrava nela e os atributos ARIA também não estavam definidos.

Neste artigo, vamos falar sobre acessibilidade e gerenciamento de foco nos componentes Dialog, Sheet e Popover. Espero que os problemas que já enfrentei ajudem você a evitá-los.


Primeiro, entenda a diferença central entre os três componentes

Para ser sincero, muita gente — inclusive eu, no passado — não distingue com clareza esses três componentes. À primeira vista, todos parecem apenas sobreposições e soam praticamente iguais. Na realidade, porém, a diferença central entre eles determina como a acessibilidade deve ser tratada.

Dialog (caixa de diálogo modal)

Dialog é uma sobreposição modal que bloqueia completamente a interação com o fundo.

Por exemplo: você clica no botão “Excluir pedido” e aparece uma caixa de diálogo de confirmação. Nesse momento, uma camada cobre a página ao fundo e você não consegue clicar em nenhum elemento dela. Essa é a característica central do Dialog: obrigar a pessoa a lidar com a tarefa atual.

Casos de uso:

  • avisos importantes (confirmação de exclusão, alertas de operação)
  • preenchimento de formulários (login, cadastro)
  • operações que exigem resposta imediata

Ponto essencial de acessibilidade: aria-modal="true" precisa estar definido e a armadilha de foco precisa ser implementada.

Sheet (painel lateral)

Sheet é um painel em formato de gaveta que desliza a partir de uma das bordas da tela. Em essência, funciona como Dialog: ambos são sobreposições modais, bloqueiam a interação com o fundo e precisam de uma armadilha de foco. A única diferença é visual: Sheet entra pela lateral, enquanto Dialog aparece no centro.

Casos de uso:

  • menu de navegação (barra lateral em dispositivos móveis)
  • painel de configurações (preferências, troca de tema)
  • exibição de detalhes (detalhes de produto, prévia de artigo)

Para ser sincero, o termo Sheet também me pareceu estranho no começo. Depois percebi que é apenas outro nome para Drawer: algumas bibliotecas de UI usam Drawer, outras usam Sheet, e Radix UI e shadcn/ui adotam Sheet.

Ponto essencial de acessibilidade: assim como em Dialog, use aria-modal="true", armadilha de foco e fechamento com Esc.

Popover (caixa flutuante)

Popover é uma sobreposição não modal, que não bloqueia a interação com o fundo.

Esse ponto é fundamental: depois que Popover é aberto, a pessoa ainda pode clicar nos elementos do fundo, e o foco não fica obrigatoriamente restrito ao Popover.

Por exemplo: você clica no botão “Mais ações” e abre um pequeno painel com “Editar”, “Copiar” e “Excluir”. Isso é um Popover. Você pode clicar em outro botão do fundo, e o Popover se fecha automaticamente.

Casos de uso:

  • menus suspensos (menus de ações, listas de opções)
  • dicas de ferramenta (explicações ricas, instruções de uso)
  • ações rápidas (editar, copiar, excluir)

Ponto essencial de acessibilidade: aria-modal="false" (ou omitido), sem armadilha de foco obrigatória e fechamento ao clicar fora.

Comparação rápida

Para ser sincero, até eu consultei esta tabela enquanto a escrevia, pois antes alguns conceitos ainda não estavam claros para mim.

CaracterísticaDialogSheetPopover
Bloqueia o fundo✅ deve bloquear✅ deve bloquear❌ não bloqueia
Armadilha de focoobrigatóriaobrigatóriaopcional (recomenda-se não forçar)
Fechamento com Escobrigatórioobrigatóriorecomendado
Fechamento ao clicar foraopcionalopcionalcomportamento padrão
Papel ARIAdialogdialogpopover
aria-modal"true""true""false" ou omitido
Posição visualcentralizadodesliza pela lateralposicionado em relação ao acionador

Em uma frase: Dialog e Sheet são sobreposições modais; Popover é uma sobreposição não modal. Sobreposições modais precisam implementar uma armadilha de foco; as não modais não precisam obrigatoriamente fazê-lo.


Padrões de acessibilidade WCAG em detalhes

Para ser sincero, os padrões WCAG parecem bem tediosos no começo. Há muitos termos em inglês, e a leitura lembra um texto jurídico. Mas, depois de enfrentar problemas em um projeto real, percebi como esses padrões são úteis: não servem apenas para passar em uma auditoria, e sim para permitir que as pessoas realmente usem a interface.

Atributos ARIA obrigatórios

Há três atributos ARIA essenciais para componentes de sobreposição:

1. role="dialog"

Esse atributo informa às tecnologias assistivas, como leitores de tela, que o elemento é uma caixa de diálogo.

<div role="dialog">
  <!-- Conteúdo da sobreposição -->
&lt;/div>

2. aria-labelledby

Esse atributo associa o título da sobreposição ao elemento. Quando ela é aberta, o leitor de tela anuncia primeiro o título.

&lt;div role="dialog" aria-labelledby="dialog-title">
  &lt;h2 id="dialog-title">Confirmar exclusão&lt;/h2>
  &lt;p>Esta ação não pode ser desfeita.&lt;/p>
&lt;/div>

3. aria-modal="true" (somente para sobreposições modais)

Esse atributo informa ao leitor de tela que o conteúdo ao fundo não está acessível.

&lt;div role="dialog" aria-modal="true">
  <!-- Conteúdo da sobreposição modal -->
&lt;/div>

Para ser sincero, eu me esquecia bastante de aria-labelledby. Só quando testei com um leitor de tela percebi o problema: sem esse atributo, ao abrir a sobreposição a pessoa não ouve nada e não sabe do que se trata.

Requisitos de navegação por teclado

A WCAG estabelece requisitos claros para a navegação por teclado em sobreposições:

Tecla Tab: fazer o foco circular dentro da sobreposição
Quando a pessoa pressiona Tab, o foco deve circular entre os elementos interativos da sobreposição, sem escapar para a página ao fundo.

Shift+Tab: fazer o foco circular no sentido inverso
Ao pressionar Shift+Tab, o foco deve percorrer os elementos na ordem inversa.

Tecla Esc: fechar a sobreposição
Quando a pessoa pressiona Esc, a sobreposição deve ser fechada. Isso é obrigatório: algumas pessoas estão acostumadas a fechar sobreposições com Esc e, se não houver suporte, ficam presas dentro delas.

Enter/Space: acionar botões
Essas duas teclas servem para ativar botões ou links.

Para ser sincero, já tive problemas com o ciclo do Tab. Quando a sobreposição abria, o foco não ficava limitado a ela e, ao pressionar Tab, ia para a página ao fundo. Esse foi justamente o problema relatado pelo cliente no início do artigo.

Regras de gerenciamento de foco

O gerenciamento de foco é a parte da acessibilidade de sobreposições mais fácil de esquecer. Os requisitos da WCAG são simples:

Ao abrir a sobreposição:
o foco deve ir para o primeiro elemento interativo dentro dela, normalmente o botão de fechar ou o primeiro campo.

Ao fechar a sobreposição:
o foco deve voltar ao elemento acionador, ou seja, ao botão que abriu a sobreposição.

Para ser sincero, antes eu nem percebia que era preciso restaurar o foco após o fechamento. Só notei ao testar com o teclado: depois que a sobreposição fechava, o foco desaparecia e a pessoa precisava encontrá-lo novamente. A experiência era realmente ruim.

Caso especial:
se a sobreposição tiver um aviso importante, como instruções de uma operação, o foco deve ir primeiro para o elemento contêiner. Assim, o leitor de tela anuncia o aviso antes de a pessoa interagir com os controles.

Para isso, adicione tabindex="0" ao contêiner:

&lt;div role="dialog" aria-modal="true" tabindex="0">
  &lt;h2>Instruções da operação&lt;/h2>
  &lt;p>Leia com atenção o conteúdo a seguir antes de continuar...&lt;/p>
  &lt;button>Confirmar&lt;/button>
&lt;/div>

Quando a sobreposição abre, o foco vai primeiro para o contêiner. O leitor de tela anuncia todo o conteúdo e, depois, a pessoa pode usar Tab para chegar ao botão.


Como funciona uma armadilha de foco

Para ser sincero, “armadilha de foco” parece algo complexo, mas o princípio é bem simples: fazer a tecla Tab circular dentro da sobreposição.

O que é uma armadilha de foco

Definição de armadilha de foco: limitar a navegação por Tab a uma área específica, fazendo o foco circular dentro dela.

Por exemplo: depois que a sobreposição abre, a pessoa pressiona Tab e o foco vai do botão “Fechar” para o botão “Confirmar”. Ao pressionar Tab novamente, ele volta para “Fechar”. Isso é uma armadilha de foco.

Ela é necessária para impedir que a pessoa acione por engano o conteúdo ao fundo. Se o foco conseguir chegar à página atrás da sobreposição, um botão do fundo pode ser acionado involuntariamente e causar uma operação inesperada.

Lógica de implementação em JavaScript

A lógica central de uma armadilha de foco é simples: localizar todos os elementos interativos da sobreposição, ouvir a tecla Tab e fazer o foco circular entre o primeiro e o último elemento.

function trapFocus(modal) {
  // Localiza todos os elementos interativos
  const focusableElements = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );

  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];

  // Ouve eventos de teclado
  modal.addEventListener('keydown', (e) => {
    if (e.key === 'Tab') {
      // Shift+Tab: no primeiro elemento, volta para o último
      if (e.shiftKey && document.activeElement === firstElement) {
        e.preventDefault();
        lastElement.focus();
      }
      // Tab: no último elemento, volta para o primeiro
      else if (!e.shiftKey && document.activeElement === lastElement) {
        e.preventDefault();
        firstElement.focus();
      }
    }

    // Esc fecha a sobreposição
    if (e.key === 'Escape') {
      closeModal();
    }
  });
}

Para ser sincero, precisei reescrever esse trecho várias vezes até funcionar. Os principais problemas foram:

  • o seletor de focusableElements precisa estar completo; se faltar algum tipo de elemento, o foco pode escapar
  • é obrigatório chamar e.preventDefault(), caso contrário o comportamento padrão do navegador leva o foco para fora

Conhecendo a biblioteca focus-trap

Se você não quiser implementar uma armadilha de foco por conta própria, pode usar uma biblioteca pronta: focus-trap-react.

import FocusTrap from 'focus-trap-react';

&lt;FocusTrap>
  &lt;div className="modal">
    &lt;button>Fechar&lt;/button>
    &lt;button>Confirmar&lt;/button>
  &lt;/div>
&lt;/FocusTrap>

Essa biblioteca cuida automaticamente do ciclo de foco, do fechamento com Esc e de sobreposições em várias camadas.

Para ser sincero, hoje quase não uso mais essa biblioteca, porque o shadcn/ui já incorpora internamente o gerenciamento de foco. O Radix UI, que serve de base para o shadcn/ui, trata toda a lógica da armadilha de foco sem exigir uma dependência adicional.


Na prática com shadcn/ui: implementando Dialog

Para ser sincero, depois que comecei a usar shadcn/ui, nunca mais implementei um componente de sobreposição do zero. Não é por preguiça: componentes escritos manualmente sempre acabam apresentando algum problema de acessibilidade, enquanto o shadcn/ui, baseado no Radix UI, cuida automaticamente desses detalhes.

Instalação e uso básico

npx shadcn@latest add dialog

Depois da instalação, o arquivo components/ui/dialog.tsx é gerado automaticamente.

Exemplo completo de código

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"

export function DeleteConfirmDialog() {
  return (
    &lt;Dialog>
      &lt;DialogTrigger asChild>
        &lt;Button variant="outline">Excluir pedido&lt;/Button>
      &lt;/DialogTrigger>
      &lt;DialogContent>
        &lt;DialogHeader>
          &lt;DialogTitle>Confirmar exclusão&lt;/DialogTitle>
          &lt;DialogDescription>
            Esta ação não pode ser desfeita. Tem certeza de que deseja excluir este pedido?
          &lt;/DialogDescription>
        &lt;/DialogHeader>
        &lt;div className="flex justify-end gap-2 mt-4">
          &lt;Button variant="outline">Cancelar&lt;/Button>
          &lt;Button variant="destructive">Excluir&lt;/Button>
        &lt;/div>
      &lt;/DialogContent>
    &lt;/Dialog>
  )
}

Para ser sincero, o código parece simples, mas o Radix UI cuida de muitos detalhes nos bastidores:

  • ao abrir a sobreposição, o foco vai para o primeiro botão (“Cancelar”)
  • ao fechá-la, o foco volta ao botão “Excluir pedido”
  • a tecla Tab circula dentro da sobreposição
  • a tecla Esc fecha a sobreposição
  • aria-labelledby é associado automaticamente ao DialogTitle
  • aria-describedby é associado automaticamente ao DialogDescription

Principais recursos de acessibilidade

1. Gerenciamento automático de foco

Quando o Dialog do Radix UI é aberto, o foco vai automaticamente para o primeiro elemento interativo da sobreposição. Quando ele é fechado, o foco volta ao elemento acionador.

2. Associação automática dos atributos ARIA

DialogTitle é associado automaticamente a aria-labelledby, e DialogDescription, a aria-describedby.

<!-- HTML gerado pelo Radix UI -->
&lt;div role="dialog" aria-modal="true" aria-labelledby="radix-:r1:" aria-describedby="radix-:r2:">
  &lt;h2 id="radix-:r1:">Confirmar exclusão&lt;/h2>
  &lt;p id="radix-:r2:">Esta ação não pode ser desfeita...&lt;/p>
&lt;/div>

Para ser sincero, é muito fácil esquecer esses detalhes ao implementar o componente manualmente. Com shadcn/ui, você não precisa se preocupar com eles.

3. Fechamento automático com Esc

Ao pressionar Esc, a sobreposição é fechada automaticamente e o foco volta ao elemento acionador.

4. Fechamento ao clicar na camada de fundo

Clicar na camada de fundo, a área cinza fora da sobreposição, também a fecha. Esse comportamento pode ser bloqueado com a propriedade onInteractOutside de DialogContent.

&lt;DialogContent onInteractOutside={(e) => e.preventDefault()}>
  <!-- Clicar na camada de fundo não fecha a sobreposição -->
&lt;/DialogContent>

Na prática com shadcn/ui: implementando Sheet

Sheet tem exatamente os mesmos recursos de acessibilidade de Dialog. A única diferença é visual: Sheet desliza a partir de uma das laterais.

Instalação e uso básico

npx shadcn@latest add sheet

Exemplo completo de código

import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
import { Button } from "@/components/ui/button"

export function NavigationSheet() {
  return (
    &lt;Sheet>
      &lt;SheetTrigger asChild>
        &lt;Button variant="outline">Abrir menu&lt;/Button>
      &lt;/SheetTrigger>
      &lt;SheetContent side="left">
        &lt;SheetHeader>
          &lt;SheetTitle>Menu de navegação&lt;/SheetTitle>
          &lt;SheetDescription>
            Escolha a página que deseja acessar
          &lt;/SheetDescription>
        &lt;/SheetHeader>
        &lt;nav className="flex flex-col gap-4 mt-4">
          &lt;a href="/" className="hover:underline">Início&lt;/a>
          &lt;a href="/about" className="hover:underline">Sobre&lt;/a>
          &lt;a href="/contact" className="hover:underline">Contato&lt;/a>
        &lt;/nav>
      &lt;/SheetContent>
    &lt;/Sheet>
  )
}

Diferenças em relação ao Dialog

Para ser sincero, os códigos de Sheet e Dialog são quase idênticos; só mudam os nomes dos componentes. As principais diferenças são:

1. Animação de entrada pela lateral

Por padrão, Sheet desliza da direita para dentro. Você pode controlar a direção com a propriedade side:

&lt;SheetContent side="left">   <!-- entra pela esquerda -->
&lt;SheetContent side="right">  <!-- entra pela direita (padrão) -->
&lt;SheetContent side="top">    <!-- entra por cima -->
&lt;SheetContent side="bottom"> <!-- entra por baixo -->

2. Os recursos de acessibilidade são os mesmos

Sheet tem exatamente os mesmos recursos de acessibilidade de Dialog:

  • role="dialog"
  • aria-modal="true"
  • armadilha de foco, fechamento com Esc e restauração do foco

Para ser sincero, uso Sheet principalmente em menus de navegação para dispositivos móveis. O efeito de entrada pela lateral combina melhor com os padrões de interação mobile.


Na prática com shadcn/ui: implementando Popover

Popover é uma sobreposição não modal. A diferença central em relação a Dialog e Sheet é: ele não bloqueia a interação com o fundo.

Instalação e uso básico

npx shadcn@latest add popover

Exemplo completo de código

import {
  Popover,
  PopoverContent,
  PopoverHeader,
  PopoverTitle,
  PopoverDescription,
  PopoverTrigger,
} from "@/components/ui/popover"
import { Button } from "@/components/ui/button"

export function ActionPopover() {
  return (
    &lt;Popover>
      &lt;PopoverTrigger asChild>
        &lt;Button variant="outline">Mais ações&lt;/Button>
      &lt;/PopoverTrigger>
      &lt;PopoverContent>
        &lt;PopoverHeader>
          &lt;PopoverTitle>Ações rápidas&lt;/PopoverTitle>
          &lt;PopoverDescription>
            Escolha uma das ações a seguir
          &lt;/PopoverDescription>
        &lt;/PopoverHeader>
        &lt;div className="flex flex-col gap-2 mt-2">
          &lt;Button size="sm">Editar&lt;/Button>
          &lt;Button size="sm">Copiar&lt;/Button>
          &lt;Button size="sm" variant="destructive">Excluir&lt;/Button>
        &lt;/div>
      &lt;/PopoverContent>
    &lt;/Popover>
  )
}

Diferenças principais

Para ser sincero, o código de Popover é parecido com o de Dialog e Sheet, mas o comportamento por trás dele é completamente diferente:

1. Não modal

Depois que Popover é aberto, a pessoa ainda pode clicar nos elementos do fundo. O foco não fica obrigatoriamente limitado ao Popover.

2. Foco não restrito

Ao pressionar Tab, o foco pode sair do Popover e chegar aos elementos do fundo. Nesse ponto, ele é totalmente diferente de Dialog.

3. Fechamento ao clicar fora

Ao clicar em qualquer elemento fora do Popover, ele é fechado automaticamente. Esse é o comportamento padrão, que pode ser bloqueado com a propriedade onInteractOutside.

&lt;PopoverContent onInteractOutside={(e) => e.preventDefault()}>
  <!-- Clicar fora não fecha o Popover -->
&lt;/PopoverContent>

4. Posicionamento flexível

Popover controla o alinhamento horizontal com a propriedade align:

&lt;PopoverContent align="start">  <!-- alinhado à esquerda -->
&lt;PopoverContent align="center"> <!-- centralizado (padrão) -->
&lt;PopoverContent align="end">    <!-- alinhado à direita -->

Para ser sincero, uso Popover principalmente em menus de ações: ao clicar em um botão, aparecem algumas opções rápidas. Esse cenário não precisa bloquear a interação com o fundo, então Popover é a escolha ideal.


Técnicas avançadas e problemas comuns

Para ser sincero, já enfrentei muitos problemas com componentes de sobreposição. Aqui estão alguns dos mais comuns.

Problema na restauração do foco: o elemento acionador foi removido

Cenário: depois que a sobreposição é aberta, o elemento acionador, como um botão, é removido. Ao fechar, não há para onde devolver o foco.

Soluções:

  1. não remova o elemento acionador; apenas o oculte
  2. ou registre um elemento de destino para receber o foco
const [triggerElement, setTriggerElement] = useState&lt;HTMLElement | null>(null);

// Registra o elemento acionador ao abrir a sobreposição
const handleOpen = (e: React.MouseEvent&lt;HTMLButtonElement>) => {
  setTriggerElement(e.currentTarget);
  setOpen(true);
};

// Devolve o foco ao fechar a sobreposição
const handleClose = () => {
  setOpen(false);
  triggerElement?.focus();
};

Para ser sincero, já enfrentei esse problema. Depois que a pessoa excluía um registro, a sobreposição fechava e o foco desaparecia. Resolvi o caso fazendo o foco voltar para o registro anterior da lista.

Problema com leitores de tela: o conteúdo da sobreposição não é anunciado

Cenário: depois que a sobreposição abre, o leitor de tela não anuncia seu conteúdo, e a pessoa não sabe o que há nela.

Causas:

  1. ausência do atributo aria-labelledby
  2. o foco não foi movido para dentro da sobreposição

Solução:
defina tanto DialogTitle quanto DialogDescription. O shadcn/ui associa automaticamente os atributos ARIA.

&lt;DialogContent>
  &lt;DialogHeader>
    &lt;DialogTitle>Confirmar exclusão&lt;/DialogTitle>  <!-- obrigatório -->
    &lt;DialogDescription>Esta ação não pode ser desfeita&lt;/DialogDescription>  <!-- obrigatório -->
  &lt;/DialogHeader>
&lt;/DialogContent>

Para ser sincero, antes eu frequentemente me esquecia de DialogDescription. Só percebi o efeito ao testar com o NVDA, um leitor de tela: sem a descrição, a pessoa conhece apenas o título da sobreposição, mas não sabe o conteúdo específico.

Problema com sobreposições aninhadas: gerenciamento de foco confuso

Cenário: a sobreposição A abre a sobreposição B. Depois que B é fechada, o foco vai para um lugar inesperado.

Solução:
Dialog e Sheet do Radix UI oferecem suporte a aninhamento. Quando a sobreposição interna é fechada, o foco volta ao acionador interno, que pode ser um botão dentro da sobreposição externa.

&lt;Dialog>
  &lt;DialogTrigger>Abrir sobreposição A&lt;/DialogTrigger>
  &lt;DialogContent>
    &lt;DialogTitle>Sobreposição A&lt;/DialogTitle>

    <!-- Abre a sobreposição B dentro da sobreposição A -->
    &lt;Dialog>
      &lt;DialogTrigger>Abrir sobreposição B&lt;/DialogTrigger>
      &lt;DialogContent>
        &lt;DialogTitle>Sobreposição B&lt;/DialogTitle>
      &lt;/DialogContent>
    &lt;/Dialog>
  &lt;/DialogContent>
&lt;/Dialog>

Para ser sincero, tento evitar sobreposições em várias camadas. Quando elas são realmente necessárias, uso o suporte a aninhamento do Radix UI para que a biblioteca cuide automaticamente do foco.

Problema com atraso de animação: o foco fica fora da sobreposição

Cenário: a sobreposição tem uma animação, como um fade-in, e durante essa animação o foco não está dentro dela.

Causa:
no início da animação, a sobreposição ainda não está totalmente visível, e a tentativa de definir o foco falha.

Solução:
o Radix UI cuida desse problema automaticamente. O foco só é definido depois que a animação termina.

Se você estiver implementando o componente por conta própria, espere a animação terminar:

modal.addEventListener('animationend', () => {
  const firstFocusable = modal.querySelector('button, [href], input');
  firstFocusable?.focus();
});

Para ser sincero, já enfrentei esse problema. Em uma sobreposição feita manualmente, o foco continuava no botão do fundo porque eu tentava defini-lo antes de a animação terminar. Só resolvi ao adicionar um ouvinte para o evento animationend.


Conclusão

Depois de tudo isso, os pontos centrais se resumem a três:

1. A diferença essencial entre os três componentes

Dialog e Sheet são sobreposições modais: bloqueiam a interação com o fundo e exigem uma armadilha de foco.
Popover é uma sobreposição não modal: não bloqueia a interação com o fundo e não restringe obrigatoriamente o foco.

2. Os três principais requisitos de acessibilidade da WCAG

Atributos ARIA (role="dialog", aria-labelledby, aria-modal="true")
Navegação por teclado (ciclo com Tab, movimento inverso com Shift+Tab e fechamento com Esc)
Gerenciamento de foco (focar ao abrir e restaurar ao fechar)

3. O shadcn/ui cuida automaticamente de todos os detalhes

O Radix UI cuida automaticamente da armadilha de foco, dos atributos ARIA e da navegação por teclado. Com shadcn/ui, você praticamente não precisa se preocupar com problemas de acessibilidade.

Para ser sincero, depois de implementar tantos componentes de sobreposição, minha regra hoje é simples: em produção, prefira shadcn/ui. Componentes escritos manualmente costumam acumular problemas de acessibilidade, enquanto o shadcn/ui, baseado no Radix UI, já trata esses detalhes.

Só há um cuidado indispensável: entenda o princípio. Quando você sabe o que o Radix UI faz nos bastidores, consegue localizar rapidamente a causa de qualquer problema.


Referências


FAQ

Qual é a diferença entre Dialog, Sheet e Popover?
Dialog e Sheet são sobreposições modais. Quando abertos, bloqueiam a interação com o fundo e precisam implementar uma armadilha de foco. Popover é uma sobreposição não modal: não bloqueia a interação com o fundo e permite que o foco se mova livremente. A diferença central é se a interação com o fundo fica bloqueada ou não.
Quais requisitos de acessibilidade um componente de sobreposição precisa atender?
A WCAG exige três aspectos principais: atributos ARIA (role="dialog", aria-labelledby e aria-modal="true"), navegação por teclado (ciclo com Tab, movimento inverso com Shift+Tab e fechamento com Esc) e gerenciamento de foco (ao abrir, mover o foco para dentro da sobreposição; ao fechar, devolvê-lo ao elemento acionador).
O que é uma armadilha de foco e por que uma sobreposição modal precisa implementá-la?
Uma armadilha de foco é uma técnica que limita a navegação por Tab a uma área específica, fazendo o foco circular dentro dela. Sobreposições modais precisam implementá-la para impedir que a pessoa acione por engano o conteúdo do fundo. Se o foco alcançar a página atrás do modal, um botão do fundo pode ser ativado involuntariamente.
Quais detalhes de acessibilidade o componente Dialog do shadcn/ui trata automaticamente?
Como o shadcn/ui é baseado no Radix UI, ele cuida automaticamente dos seguintes detalhes:

• ao abrir a sobreposição, o foco vai para o primeiro elemento interativo
• ao fechar, o foco volta ao elemento acionador
• a tecla Tab circula dentro da sobreposição
• a tecla Esc fecha a sobreposição
• aria-labelledby é associado automaticamente ao DialogTitle
• aria-describedby é associado automaticamente ao DialogDescription
Para onde o foco deve voltar depois que a sobreposição é fechada?
O foco deve voltar ao elemento acionador, ou seja, ao botão que abriu a sobreposição. Esse é um requisito explícito da WCAG. Se o acionador tiver sido removido, como em uma operação de exclusão, o foco deve ir para o próximo elemento lógico, por exemplo o registro anterior da lista.
O que fazer quando uma animação impede a definição do foco?
No início da animação, a sobreposição talvez ainda não esteja totalmente visível e a definição do foco pode falhar. A solução é esperar a animação terminar antes de definir o foco, ouvindo o evento animationend. O Radix UI cuida disso automaticamente.
Como fazer o leitor de tela anunciar o conteúdo da sobreposição?
Defina tanto DialogTitle quanto DialogDescription. O shadcn/ui associa automaticamente aria-labelledby e aria-describedby. Se a sobreposição contiver um aviso importante, você pode adicionar tabindex="0" ao contêiner para que ele receba o foco primeiro e o leitor de tela anuncie todo o conteúdo antes dos controles.

18 min de leitura · Publicado em: 29 mar 2026 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog