Dialog
shadcn Dialog (modal) - trigger, estado controlado, formulários dentro de diálogos e acessibilidade.
Busque em todas as páginas da documentação
shadcn Dialog (modal) - trigger, estado controlado, formulários dentro de diálogos e acessibilidade.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
npx shadcn@latest add dialog button input labelimport {
Dialog, DialogContent, DialogDescription, DialogFooter,
DialogHeader, DialogTitle, DialogTrigger, DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
// Diálogo básico
<Dialog>
<DialogTrigger asChild>
<Button>Abrir Diálogo</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Tem certeza?</DialogTitle>
<DialogDescription>Esta ação não pode ser desfeita.</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancelar</Button>
</DialogClose>
<Button>Confirmar</Button>
</DialogFooter>
</DialogContent>
</Dialog>Quando usar isso: Quando você precisar de uma sobreposição modal para confirmações, formulários, visualizações de detalhes ou qualquer conteúdo que exija a atenção focada do usuário.
"use client";
import { useState } from "react";
import {
Dialog, DialogContent, DialogDescription, DialogFooter,
DialogHeader, DialogTitle, DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const EditProfileSchema = z.object({
name: z.string().min(1, "O nome é obrigatório"),
email: z.string().email("Email inválido"),
bio: z.string().max(200, "A bio deve ter no máximo 200 caracteres").optional(),
});
type EditProfileData = z.infer<typeof EditProfileSchema>;
export function EditProfileDialog({
profile,
onSave,
}: {
profile: EditProfileData;
onSave: (data: EditProfileData) => Promise<void>;
}) {
const [open, setOpen] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm<EditProfileData>({
resolver: zodResolver(EditProfileSchema),
defaultValues: profile,
});
async function onSubmit(data: EditProfileData) {
await onSave(data);
setOpen(false);
}
function handleOpenChange(nextOpen: boolean) {
setOpen(nextOpen);
if (nextOpen) {
reset(profile); // reseta o formulário ao abrir
}
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button variant="outline">Editar Perfil</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<form onSubmit={handleSubmit(onSubmit)}>
<DialogHeader>
<DialogTitle>Editar Perfil</DialogTitle>
<DialogDescription>
Atualize as informações do seu perfil. Clique em salvar quando terminar.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="name">Nome</Label>
<Input id="name" {...register("name")} />
{errors.name && (
<p className="text-sm text-red-600">{errors.name.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" {...register("email")} />
{errors.email && (
<p className="text-sm text-red-600">{errors.email.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="bio">Bio</Label>
<Input id="bio" {...register("bio")} placeholder="Conte-nos sobre você" />
{errors.bio && (
<p className="text-sm text-red-600">{errors.bio.message}</p>
)}
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
Cancelar
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Salvando..." : "Salvar"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}O que isso demonstra:
open e onOpenChange@radix-ui/react-dialog - totalmente acessível com trap de foco e fechamento com ESCDialogTrigger abre o diálogo quando clicadoDialogContent renderiza em um portal, sobrepondo a página com um fundoDialogClose renderiza um botão que fecha o diálogo quando clicadoasChild mescla as props do componente em seu único elemento filhoDiálogo de confirmação com ação assíncrona:
function DeleteConfirmDialog({
onConfirm,
itemName,
}: {
onConfirm: () => Promise<void>;
itemName: string;
}) {
const [open, setOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
async function handleDelete() {
setDeleting(true);
await onConfirm();
setDeleting(false);
setOpen(false);
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="destructive" size="sm">Excluir</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Excluir {itemName}?</DialogTitle>
<DialogDescription>
Esta ação não pode ser desfeita. Isso excluirá permanentemente o item.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)} disabled={deleting}>
Cancelar
</Button>
<Button variant="destructive" onClick={handleDelete} disabled={deleting}>
{deleting ? "Excluindo..." : "Excluir"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}Diálogo com conteúdo rolável:
<DialogContent className="max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Termos de Serviço</DialogTitle>
</DialogHeader>
<div className="prose max-w-none text-sm">
{/* Conteúdo longo */}
</div>
</DialogContent>Impedir fechamento ao clicar no fundo:
<DialogContent
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
{/* O usuário deve fechar explicitamente */}
</DialogContent>// Props do componente Dialog
import type { DialogProps } from "@radix-ui/react-dialog";
// Wrapper de diálogo controlado
interface ConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
title: string;
description: string;
}
// DialogContent estende Radix DialogContentProps
// Você pode passar quaisquer atributos válidos de div HTMLSubmissão de formulário fecha o diálogo - Se a ação do formulário navegar ou o estado mudar, o diálogo pode fechar inesperadamente. Correção: Use estado controlado e feche apenas após a conclusão da ação.
Trap de foco com portais - Se você renderizar um dropdown ou popover dentro de um diálogo, certifique-se de que ele também seja renderizado em portal corretamente. Os componentes shadcn lidam com isso automaticamente.
Rolagem em dispositivos móveis - O conteúdo do diálogo pode transbordar em telas pequenas. Correção: Adicione max-h-[85vh] overflow-y-auto ao DialogContent.
Múltiplos diálogos empilhados - Abrir um diálogo de dentro de outro funciona, mas pode confundir os usuários. Correção: Use um único diálogo com conteúdo dinâmico, ou garanta o empilhamento correto de z-index.
Reset do formulário ao fechar - O conteúdo do diálogo permanece montado entre as aberturas por padrão. Correção: Resete o estado do formulário no onOpenChange quando o diálogo abrir.
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| Sheet (painel lateral) | Você quer um painel deslizante em vez de um modal centralizado | Um diálogo centralizado é mais apropriado |
| Alert Dialog | A ação é destrutiva e não deve ser acidentalmente descartada | O diálogo pode ser fechado clicando fora |
| Popover | Você precisa de uma sobreposição pequena e não bloqueante perto de um trigger | Você precisa de um lock de foco em página inteira |
| Drawer | Você quer um bottom sheet amigável para dispositivos móveis | Experiência primariamente para desktop |
DialogTrigger; o Dialog gerencia seu próprio estado aberto/fechadoopen e onOpenChange para Dialog para controle total do estadoasync function onSubmit(data: FormData) {
await saveData(data);
setOpen(false); // fecha apenas após o sucesso
}Use estado controlado (open/onOpenChange) para que você decida quando fechar.
@radix-ui/react-dialog<DialogContent
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
{/* O usuário deve clicar explicitamente em um botão de fechar */}
</DialogContent>onOpenChange quando o diálogo abrirmax-h-[85vh] overflow-y-auto ao DialogContent<Button>) substitui o elemento renderizado padrãoasChild, DialogTrigger renderiza seu próprio botão envolvendo seu filhointerface ConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
title: string;
description: string;
}DialogClose renderiza um botão que fecha automaticamente o diálogo ao clicarButton regular com onClick={() => setOpen(false)} em vez dissoconst [deleting, setDeleting] = useState(false);
async function handleDelete() {
setDeleting(true);
await onConfirm();
setDeleting(false);
setOpen(false);
}
// Desabilita os botões Cancelar e Excluir enquanto excluiDialogProps de @radix-ui/react-dialog para as props do componente raizDialogContent estende DialogContentProps do Radix e aceita todos os atributos válidos de div HTMLRevisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥