Skip to content

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érioAção
≤ 4 campos / altura < 600pxDensidade padrão (sem alterações)
5 campos OU preview pesadoAvaliar caso a caso
6+ campos OU > 600px estimadoAplicar 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

tsx
<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-0 no DialogContent — anula padding global; cada zona aplica o seu.
  • flex flex-col no DialogContent — destrava o layout vertical. A primitiva usa grid por default, o que impede overflow-y-auto interno de receber altura → footer recortado em viewports baixas.
  • flex-1 min-h-0 no corpo — sem min-h-0, o flex item nunca encolhe abaixo do seu conteúdo natural, e o overflow não dispara.
  • shrink-0 em header/footer — impede que o flex distribua espaço para essas zonas; só o corpo flexa.
  • overflow-hidden no 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-t com border/50 — separação visual sutil entre zonas, mantém continuidade.

4. Tokens compactos (densidade reduzida proporcional)

ElementoPadrão shadcnPadrão denso
Largura do modalmax-w-2xl (672px)max-w-xl (576px)
Espaço vertical entre camposspace-y-4 (16px)space-y-3 (12px)
Espaço label↔inputspace-y-2 (8px)space-y-1.5 (6px)
Gap horizontal em gridgap-4gap-3
Labelpadrão (14px)text-xs (12px)
Input / Select / Textareah-10 padrãoh-9 text-sm
Textarea rows32
Botões footerpadrãosize="sm"
Texto auxiliar (<p>)text-xstext-[11px]
Padding interno previewp-4p-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 AlertDialog padrã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

tsx
{/* ❌ 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

ArquivoPadrão aplicadoNº de campos
src/components/header-novidades-especialista/modal-editor.tsx3-zonas + tokens compactos7
src/components/agenda/agenda-nova-tarefa-dialog.tsxVariante (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-hidden no DialogContent?
  • [ ] overflow-y-auto somente 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: