Modal
Em andamentoJanela sobre a página que pede atenção ou decisão: prende o foco dentro dela, torna o resto da página inert, trava a rolagem do fundo, fecha com Esc, com o X ou com clique fora (que se pode desligar) e devolve o foco a quem abriu. O título é obrigatório; role="alertdialog" para a confirmação que interrompe.
- Função
- Estado e aviso
- Também chamado de
- diálogo, janela, pop-up, modal, caixa de diálogo
- Mora em
- app/components/molecules/Modal.tsx
- Última mudança
- 2026-10-01
Prévia
A peça ao vivo, vinda da ficha, na largura escolhida. Em larguras fixas, ela roda num quadro do tamanho de uma tela de verdade.
1280px, em tamanho real
Variantes na ficha
Lidas da ficha no build, na ordem do palco. Cada uma abre sozinha, sem o styleguide em volta — é o endereço que o palco carrega.
Quando usar
Para uma decisão ou um formulário curto que precisa ser resolvido antes de seguir — confirmar a matrícula, excluir algo.
Faça
- Escreva um
titleque diga o que se decide ("Confirme sua matrícula"). - Ponha as ações no
footer, com a principal por último. - Deixe o botão de fechar (
showCloseButton), a menos que a decisão seja obrigatória. - Use
sizepela quantidade de conteúdo:smaxl, oufull. - Em confirmação destrutiva,
role="alertdialog"ecloseOnBackdrop={false}: um clique errado no fundo não decide por ninguém (E63). - Use a origem do
onClose(botao,fundo,escape) quando o fechamento precisa de tratamento — perguntar antes de descartar um rascunho, por exemplo.
Evite
- Modal dentro de modal — um de cada vez.
- Fluxo de várias etapas dentro do diálogo: aí é página.
- Tirar toda saída: sem X, o Esc e uma ação de cancelar continuam obrigatórios.
- Abrir sem ação da pessoa (ao carregar a página).
- Renderizar solto no styleguide: ele toma a página — por isso a prévia dele abre num documento próprio.
Quando não usar. Para uma mensagem que não pede ação, use o Alert. Para escolher entre ações, o Dropdown. Para conteúdo longo, uma página.
Nota de uso
Prende o foco, devolve-o a quem abriu e torna o resto da página inert. Como (corrigido na E63): o diálogo vai por portal direto ao <body>, e o inert cai em todos os irmãos dele — o fundo inteiro, sem precisar subir ancestrais; a nota antiga descrevia uma cadeia de ancestrais que o código não usa. Um de cada vez, sempre com saída explícita (Esc e uma ação de cancelar, mesmo sem o X), e nunca fluxo de várias etapas. role="alertdialog" e closeOnBackdrop={false} para confirmação destrutiva. A entrada — esmaece e cresce de 95% a 100% em 200ms — só anima com movimento liberado no sistema; até a E46 a classe vinha de um plugin nunca instalado e o painel aparecia seco. Desde a E113 essa mecânica mora em app/lib/dialogo-modal.ts, que a Gaveta também usa: uma cópia só, com os testes das duas peças travando.
Anatomia
As partes, na ordem em que o olho — e o teclado — passam.
- Fundo — Escurece a página a 60% com desfoque. É só desenho: o clique fora do painel fecha pelo documento — o fechamento leve do `<dialog>` nativo (E137) —, salvo com `closeOnBackdrop={false}`.
- Painel — `--card`, cantos `rounded-xl`, na largura do `size`.
- Cabeçalho — Título (`h2`), descrição opcional e o botão "Fechar" de 44×44.
- Conteúdo — O `children`, com rolagem própria quando passa da altura.
- Rodapé — O `footer`, com as ações.
Estados
Os estados que a peça tem no código, e o que muda em cada um.
| Estado | O que muda |
|---|---|
| Fechado | Nada é renderizado. |
| Aberto | Portal no <body>, role="dialog" com aria-modal, resto da página inert e rolagem travada. Entra esmaecendo e crescendo de 95% em 200ms, só com movimento liberado. |
| Sem nada focável | O foco vai ao próprio painel, e o Tab não sai dele — conferido no teste da ficha. |
| Alerta | role="alertdialog": o leitor anuncia como alerta e lê a descrição junto (E63). |
Comportamento
| Situação | O que acontece |
|---|---|
| Abre | Guarda quem tinha o foco e leva o foco ao primeiro elemento do conteúdo, pulando o "Fechar" — conferido nos testes da ficha: "Voltar" num diálogo de confirmação, o primeiro campo num formulário. |
| Fecha | Devolve o foco a quem abriu — pelo Esc e pelo fundo, conferido no teste da ficha —, destrava a rolagem e tira o inert da página. |
| Clique no fundo | Chama onClose("fundo"); com closeOnBackdrop={false}, não fecha, e o foco fica onde estava, dentro do diálogo — conferido no teste da ficha. Até a medição da E63 esse clique mandava o foco ao <body>. |
| Esc ou X | Chama onClose("escape") ou onClose("botao") (E63). |
| Elemento escondido dentro | Fica fora da volta do Tab (o filtro descarta o que não tem offsetParent). |
Relacionados
Como usar no código
A importação e o menor uso, lidos do arquivo. O … marca as props obrigatórias.
import { Modal } from '@/app/components/molecules/Modal';
<Modal isOpen={…} onClose={…} title={…}>…</Modal>Propriedades
Lidas do código da peça no build — não escritas à mão.
| Prop | Tipo | Padrão | Obrigatória | Descrição |
|---|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'md' | não | Variante do cva. |
isOpen | boolean | — | sim | — |
onClose | (origem: OrigemDoFechamento) => void | — | sim | Recebe a origem (E63): 'botao' (o X), 'fundo' ou 'escape'. Quem fecha por um botão do rodapé chama o próprio estado — não passa por aqui. |
role | 'dialog' | 'alertdialog' | 'dialog' | não | alertdialog (E63) para confirmação que interrompe — excluir, sair sem salvar. O leitor anuncia como alerta, e a descrição é lida junto. |
closeOnBackdrop | boolean | true | não | Clique no fundo fecha (padrão). Desligue em confirmação destrutiva e em formulário longo, em que um clique errado perde o que se escreveu (E63). O Esc e o X continuam fechando: saída explícita sempre existe. |
title | string | — | sim | Obrigatório de propósito. Um diálogo sem nome é anunciado pelo leitor de tela apenas como "diálogo" — a pessoa sabe que algo abriu, mas não o quê. Isso é defeito de conteúdo, não opção de API, e o tipo é o lugar certo para dizer isso: o erro aparece na hora de escrever a chamada, não numa auditoria seis meses depois. |
description | string | — | não | — |
children | React.ReactNode | — | sim | — |
footer | React.ReactNode | — | não | — |
showCloseButton | boolean | true | não | — |
Tokens de cor
Os tokens semânticos que o código da peça usa, com o valor de hoje em app/globals.css.
| Token | Valor | Usado como |
|---|---|---|
--border | #E5E5E5 | border-border |
--card | #FFFFFF | bg-card |
--foreground | #1A1A1A | text-ink |
--muted | #F4F6F9 | bg-muted |
--muted-foreground | #595959 | text-muted-foreground |
Teclado
| Tecla | Faz |
|---|---|
| Tab | Anda pelos elementos do painel; do último volta ao primeiro — medido: Cancelar, Confirmar Ação, Fechar, Cancelar… |
| ShiftTab | Anda para trás; do primeiro vai ao último. |
| Esc | Fecha e devolve o foco ao botão que abriu — medido. |
Leitor de tela
O que é anunciado, e por quê.
- O nome do diálogo é o título (
aria-labelledby), e a descrição, se houver, é ligada poraria-describedby. - O
inertno resto da página tira o fundo do Tab e da árvore de acessibilidade — o leitor não "escapa" do diálogo. O diálogo vai por portal ao<body>, e oinertcai nos irmãos dele — conferido no teste da ficha.
O que sustenta o estado
Esta peça usa dialogo. O estado dela é o de quem já foi ouvido com leitor de tela nesses padrões — e não uma passada sobre esta peça em específico.
Falta ouvir: dialogo — ver docs/a11y-screen-reader-checklist.md.
Status e recursos
| Tipo | Recurso | Status |
|---|---|---|
| Código | app/components/molecules/Modal.tsx | Disponível |
| Ficha | Storybook (abre em outra aba) | Disponível |
| Teste de acessibilidade | axe em npm run test | No gate de todo commit |
| Leitor de tela | Passada da E33 | Falta ouvir |
| Design | Figma | Desativado até a Fase 2 |
Histórico
Os últimos commits que tocaram o arquivo, lidos do git no build.
251416b2026-10-01E159: Testimonials — o vídeo toca dentro do cartão, YouTube e Vimeo
O histórico completo está no GitHub (abre em outra aba).