Padrão de Modais Densos — Frontend
Padrão SSOT para modais com 6 ou mais campos ou altura estimada > 600px, garantindo que caibam em qualquer viewport sem recortes no topo/rodapé.
Este padrão complementa (não substitui)
FRONTEND_UI_STANDARDS.md §5 (Modais). Para modais com ≤ 4 campos, mantenha a densidade padrão do shadcn.
1. Causa raiz
A primitiva DialogContent (src/components/ui/dialog.tsx) é centralizada via flex items-center justify-center p-4 no overlay e não possui scroll interno. Modais grandes ultrapassam a altura útil em viewports ≤ 720px (notebooks padrão), causando recorte do header (título) e/ou do footer (botões).
Não corrigir na primitiva. Ela é compartilhada por dezenas de modais; alterar comportamento global = regressão sistêmica. A correção é localizada no consumer que sabe que tem muitos campos.
2. Quando aplicar
| Critério | Ação |
|---|---|
| ≤ 4 campos / altura < 600px | Densidade padrão (sem alterações) |
| 5 campos OU preview pesado | Avaliar caso a caso |
| 6+ campos OU > 600px estimado | Aplicar este padrão obrigatoriamente |
3. Estrutura padrão — 3 zonas
┌─────────────────────────────────────┐
│ HEADER fixo (border-b) │ ← DialogHeader sticky
├─────────────────────────────────────┤
│ │
│ CORPO scrollável (overflow-y-auto) │ ← Apenas esta zona rola
│ Todos os campos do formulário │
│ │
├─────────────────────────────────────┤
│ FOOTER fixo (border-t) │ ← Botões sempre visíveis
└─────────────────────────────────────┘Esqueleto canônico
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
<DialogContent className="max-w-xl max-h-[calc(100dvh-2rem)] overflow-hidden p-0 gap-0 flex flex-col">
{/* Zona 1 — Header fixo */}
<DialogHeader className="px-5 pt-5 pb-3 border-b border-border/50 shrink-0">
<DialogTitle className="text-base">Título</DialogTitle>
<DialogDescription className="text-xs">Subtítulo</DialogDescription>
</DialogHeader>
{/* Zona 2 — Corpo scrollável */}
<div className="flex-1 min-h-0 overflow-y-auto px-5 py-4 space-y-3">
{/* todos os campos */}
</div>
{/* Zona 3 — Footer fixo */}
<div className="px-5 py-3 border-t border-border/50 flex justify-end gap-2 shrink-0">
<Button size="sm" variant="outline">Cancelar</Button>
<Button size="sm">Confirmar</Button>
</div>
</DialogContent>
</Dialog>Pontos críticos:
p-0 gap-0noDialogContent— anula padding global; cada zona aplica o seu.flex flex-colnoDialogContent— destrava o layout vertical. A primitiva usagridpor default, o que impedeoverflow-y-autointerno de receber altura → footer recortado em viewports baixas.flex-1 min-h-0no corpo — semmin-h-0, o flex item nunca encolhe abaixo do seu conteúdo natural, e o overflow não dispara.shrink-0em header/footer — impede que o flex distribua espaço para essas zonas; só o corpo flexa.overflow-hiddenno container — impede vazamento; só o corpo rola.max-h-[calc(100dvh-2rem)]—dvh(dynamic viewport height) respeita barra dinâmica de browsers móveis.border-b/border-tcomborder/50— separação visual sutil entre zonas, mantém continuidade.
4. Tokens compactos (densidade reduzida proporcional)
| Elemento | Padrão shadcn | Padrão denso |
|---|---|---|
| Largura do modal | max-w-2xl (672px) | max-w-xl (576px) |
| Espaço vertical entre campos | space-y-4 (16px) | space-y-3 (12px) |
| Espaço label↔input | space-y-2 (8px) | space-y-1.5 (6px) |
| Gap horizontal em grid | gap-4 | gap-3 |
Label | padrão (14px) | text-xs (12px) |
Input / Select / Textarea | h-10 padrão | h-9 text-sm |
Textarea rows | 3 | 2 |
| Botões footer | padrão | size="sm" |
Texto auxiliar (<p>) | text-xs | text-[11px] |
| Padding interno preview | p-4 | p-3 |
A redução é proporcional: nenhum elemento fica visualmente "minúsculo", apenas mais denso.
5. Quando NÃO aplicar
- Modais com até 4 campos: densidade padrão é mais legível.
- Modais informativos (alert, confirmação): use
AlertDialogpadrão. - Modais com 1 campo principal (ex: pedir nome): densidade padrão.
Se um modal denso precisar de um campo "destaque" (ex: título principal grande), pode-se usar text-sm localmente — mas labels e inputs secundários seguem text-xs / h-9.
6. Anti-padrões
{/* ❌ Sem scroll interno → recorta em viewport pequeno */}
<DialogContent className="max-w-2xl">
<DialogHeader>...</DialogHeader>
<div className="space-y-4">
{/* 7 campos */}
</div>
<Button>Salvar</Button>
</DialogContent>
{/* ❌ overflow-y-auto no DialogContent inteiro → header e footer rolam junto */}
<DialogContent className="max-h-[80vh] overflow-y-auto">
...
</DialogContent>
{/* ❌ Alterar a primitiva ui/dialog.tsx para "resolver" o problema */}
{/* Quebra dezenas de outros modais que dependem da altura natural. */}
{/* ❌ Usar 100vh em vez de 100dvh em mobile */}
<DialogContent className="max-h-[calc(100vh-2rem)]"> {/* não respeita barra dinâmica */}7. Referências de implementação
| Arquivo | Padrão aplicado | Nº de campos |
|---|---|---|
src/components/header-novidades-especialista/modal-editor.tsx | 3-zonas + tokens compactos | 7 |
src/components/agenda/agenda-nova-tarefa-dialog.tsx | Variante (max-h-[calc(100vh-2rem)] overflow-hidden) | 6+ |
8. Checklist de adoção
Antes de criar/refatorar um modal denso:
- [ ] Contagem ≥ 6 campos OU altura estimada > 600px?
- [ ] Aplicou estrutura 3-zonas (
p-0 gap-0+ header/corpo/footer separados)? - [ ]
max-h-[calc(100dvh-2rem)] overflow-hiddennoDialogContent? - [ ]
overflow-y-autosomente no corpo? - [ ] Tokens compactos aplicados (h-9, text-xs, space-y-3, size="sm")?
- [ ] Testado em viewport 1024×600 (notebook pequeno)?
- [ ] Testado em viewport 768×1024 (tablet vertical)?
Referências cruzadas:
FRONTEND_UI_STANDARDS.md§5 — Modais (regra geral)ATOMIC_RENDERING.md— Anti-flash em loadingmem://ui/modal-density-and-scroll-standard— Regra persistente