Dialog
Modal centrado para decisões focadas e fluxos curtos — com modelo de 3 tiers para confirmações.
Overview
Dialog é o overlay centrado e bloqueante do KDS — montado sobre um
overlay de 50% de opacidade, recebe foco automaticamente e prende o
foco até o usuário fechar. Use para decisões focadas ou fluxos
curtos que precisam interromper o contexto: confirmar uma ação,
editar um item, mostrar um detalhe específico.
A escolha entre Dialog, Sheet, Popover e Drawer é uma escolha de peso da interrupção:
| Quando o conteúdo… | Use |
|---|---|
| precisa de foco total e é curto (1–3 campos, 1 decisão) | Dialog |
| é um fluxo longo / múltiplas seções (filtros, settings) | Sheet |
| é inline e contextual (date picker, mini form) | Popover |
| é mobile-first com gesto de arrastar | Drawer |
Anatomy
<Dialog>
<DialogTrigger /> ← visível, no fluxo
<DialogContent> ← portalado, centralizado
<DialogHeader>
<DialogTitle /> ← pergunta-pergunta, frase clara
<DialogDescription /> ← contexto breve
</DialogHeader>
{/* corpo do dialog */}
<DialogFooter>
<DialogClose>Cancelar</DialogClose> ← saída segura à esquerda
<Button>Confirmar</Button> ← ação primária à direita
</DialogFooter>
</DialogContent>
</Dialog>A anatomia é estrita: título como pergunta direta, descrição
opcional para contexto, footer com saída à esquerda + ação à direita
(convenção ocidental). O DialogContent já cuida de overlay, focus
trap, animação e o X no canto superior — passe showCloseButton={false}
quando o X for redundante (poucos casos).
Subcomponents
| Componente | Descrição |
|---|---|
Dialog | Raiz que controla open/closed. |
DialogTrigger | Elemento clicável que abre o dialog. |
DialogContent | Container portalado com overlay e focus trap. |
DialogHeader | Wrapper de título + descrição. |
DialogTitle | Título obrigatório (semântica). |
DialogDescription | Descrição opcional anunciada por SR. |
DialogFooter | Wrapper das ações no rodapé. |
DialogClose | Wrapper que fecha o dialog ao clicar. |
DialogPortal / DialogOverlay | Expostos para casos avançados. |
Usage
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose
} from "@kalvner/kds/overlays/dialog";
import { Button } from "@kalvner/kds/forms/button";
export function ConfirmPublish() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Publicar</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Publicar este post?</DialogTitle>
<DialogDescription>
Ele ficará visível para todos os assinantes.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost">Cancelar</Button>
</DialogClose>
<Button>Publicar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}Stakes
Confirmações não são todas iguais. KDS usa um modelo de 3 tiers para escolher quanto atrito impor antes de uma ação destrutiva. Cada tier tem um padrão visual específico — copie a forma certa para o nível de risco, nem mais, nem menos.
Tier 1 — Low stakes
Ação reversível e de baixo impacto. Excluir um comentário, descartar um rascunho que pode ser reescrito, dispensar uma notificação. Use um Dialog comum com botão default (não destructive) — o objetivo é só garantir que não foi clique acidental.
Tier 2 — Medium stakes
Destrutivo mas recuperável. Arquivar um projeto, expulsar um
membro, mover algo para a lixeira. O Dialog ganha um botão
destructive (vermelho), e a descrição lista o que vai acontecer.
Combine sempre com um undo via Sonner toast logo após a
operação — Linear faz isso para movimentação de issues, Gmail para
arquivamento. O par dialog → toast com Desfazer é o que torna o
gesto seguro.
Tier 3 — High stakes
Destrutivo e irreversível. Excluir conta, deletar repositório,
limpar histórico. O usuário precisa digitar o nome do recurso (ou
uma palavra-chave como DELETE) para habilitar o botão. Padrão
canônico — GitHub para deletar repos, Stripe para fechar contas,
Linear para projetos. O objetivo não é só evitar clique acidental;
é forçar atenção plena a um gesto que não tem volta.
DELETE para habilitar.Decision matrix
| Reversibilidade | Impacto baixo | Impacto alto |
|---|---|---|
| Reversível | Tier 1 — Dialog comum | Tier 2 — Dialog destructive + undo |
| Irreversível | Tier 2 — Dialog destructive | Tier 3 — Typed confirmation |
Quando ficar em dúvida, suba um tier. O custo de fricção extra é
sempre menor do que o custo de uma exclusão acidental — sobretudo em
contextos profissionais onde não há "ctrl-Z global". Para confirmações
de Tier 2 e Tier 3, considere usar AlertDialog
em vez de Dialog — AlertDialog é não-cancelável por overlay click,
forçando uma escolha explícita.
When to use
- Confirmar uma ação que pode causar surpresa ou perda.
- Apresentar um formulário curto (1–3 campos) que precisa de foco.
- Mostrar detalhes de um item em modo focado, sem trocar de página.
- Onboarding com 1–2 passos rápidos.
When not to use
- Para fluxos longos ou multi-seção, use
Sheet. - Para conteúdo contextual e inline, use
Popover. - Para feedback transiente após uma ação concluída, use
Sonner(toast). - Para alertas críticos que não permitem cancelar por overlay
click, use
AlertDialog. - Nunca aninhe Dialogs. Se o segundo passo precisa de outro contexto pesado, repense o fluxo (wizard inline, navegação, Sheet).
Best practices
- Título como pergunta direta. "Publicar este post?" lê melhor do que "Confirme a publicação". Verbo na frente, recurso explícito.
- Descrição é contexto, não muleta. Se o título não disser claramente o que acontece, conserte o título primeiro.
- Ordem ocidental no footer. Cancelar à esquerda, ação à direita. O olho percorre da esquerda para a direita até o commit.
- Overlay click = cancelar. O comportamento padrão fecha o dialog sem confirmar. Não desabilite isso em Tier 1.
- Foco automático. Não force foco em campos perigosos. Um campo
vazio com auto-focus no botão
destructiveé uma armadilha. - Anuncie mudanças. Se o conteúdo do dialog muda dinamicamente
(ex.: campo de typed confirmation), use
aria-livepara que SRs acompanhem. - Não aninhe. Dois dialogs sobrepostos confundem foco e leitura por screen reader. Em vez disso, use steps inline ou navegação.
Accessibility
| Concern | Comportamento |
|---|---|
| Roles | Radix Dialog aplica role="dialog" + aria-modal="true". |
| Foco | Foco entra automaticamente no primeiro elemento focável; preso até o close (focus trap). Volta para o trigger ao fechar. |
| Keyboard | Esc fecha. Tab/Shift+Tab cicla entre os focáveis dentro do dialog. |
| Screen reader | DialogTitle é obrigatório — anunciado como nome do dialog. DialogDescription é anunciado quando vinculado via aria-describedby (Radix faz isso automaticamente). |
| Layout shift | Como o dialog é portalado e o body recebe overflow: hidden, há uma gutter de scrollbar — Radix compensa com padding-right para evitar CLS. Ver [[ui/layout/cumulative-layout-shift]]. |
| Touch target | Botões no footer respeitam o tamanho mínimo do Button primitive (36px / lg = 40px). |
| Reduced motion | A animação enter/exit usa data-[state=open/closed]:animate-in/out; respeite prefers-reduced-motion no nível do tema. |
Related
AlertDialog— variante modal+blocking, usada para Tier 2 e Tier 3 quando o usuário precisa decidir.Sheet— para fluxos longos ou painéis laterais.Drawer— bottom sheet para mobile.Sonner— toasts transientes; o canal natural para fechar o loop "Excluído. Desfazer."Button— variantes default e destructive usadas no footer.