Dialog
A modal dialog that overlays the main content and requires user interaction. Based on Radix UI Dialog with full accessibility support.
Features
Built on Radix UI Dialog primitives. Fully accessible with ARIA attributes and keyboard support. Focus trap prevents focus from leaving the dialog. Closes on Escape key or backdrop click. Supports nested dialogs. Can be controlled or uncontrolled. Customizable open/close animations. Restores focus to trigger element on close.
Installation
Install the component from your command line.
ng g @ng-cn/core:c dialogAnatomy
Import all parts and piece them together.
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose
} from '@/ui/dialog';<Dialog>
<DialogTrigger>
Open Dialog
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>Dialog description text</DialogDescription>
</DialogHeader>
<div>Dialog content goes here</div>
<DialogFooter>
<DialogClose>Close</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>API Reference
Root
The root component that manages dialog state and context.
| Prop | Type | Default |
|---|---|---|
| defaultOpen | boolean | false |
| The open state of the dialog when it is initially rendered. Use when you do not need to control the state. | ||
| open | boolean | — |
| The controlled open state of the dialog. Can be bound with [(open)]="myOpen" or use [open] with (openChange). | ||
| modal | boolean | true |
| The modality of the dialog. When true, interaction with outside elements will be disabled and only dialog content will be visible to screen readers. | ||
| openChange | EventEmitter<boolean> | — |
| Event emitted when the dialog open state changes. |
Data Attributes
| Attribute | Values |
|---|---|
| [data-state] | "open" | "closed" |
Trigger
The button that opens the dialog. This element must be rendered.
| Prop | Type | Default |
|---|---|---|
| asChild | boolean | false |
| Change the default rendered element for the one passed as a child, merging their props and behavior. |
Data Attributes
| Attribute | Values |
|---|---|
| [aria-haspopup] | "dialog" |
| [aria-expanded] | true | false |
| [aria-controls] | id of DialogContent |
Content
The dialog modal content. Should contain DialogHeader, content, and DialogFooter.
| Prop | Type | Default |
|---|---|---|
| class | string | — |
| Additional CSS classes to apply to the content container. | ||
| showClose | boolean | true |
| Whether to display the close button in the top right corner. | ||
| initialFocus | string | — |
| CSS selector for the element that should receive focus when the dialog opens. |
Data Attributes
| Attribute | Values |
|---|---|
| [data-state] | "open" | "closed" |
| [role] | "dialog" |
| [aria-modal] | true |
| [aria-labelledby] | id of DialogTitle |
| [aria-describedby] | id of DialogDescription |
CSS Variables
| Variable | Description |
|---|---|
| --dialog-animation-duration | Duration of entry and exit animations |
Header
Container for the dialog title and description.
| Prop | Type | Default |
|---|---|---|
| class | string | — |
| Additional CSS classes to apply to the header. |
Title
The accessible title of the dialog. Automatically connected via ARIA.
| Prop | Type | Default |
|---|---|---|
| class | string | — |
| Additional CSS classes to apply to the title. |
Data Attributes
| Attribute | Values |
|---|---|
| [id] | auto-generated unique id |
Description
Optional accessible description of the dialog.
| Prop | Type | Default |
|---|---|---|
| class | string | — |
| Additional CSS classes to apply to the description. |
Data Attributes
| Attribute | Values |
|---|---|
| [id] | auto-generated unique id |
Footer
Container for dialog action buttons, typically placed at the bottom.
| Prop | Type | Default |
|---|---|---|
| class | string | — |
| Additional CSS classes to apply to the footer. |
Close
A button that closes the dialog. Must be rendered within the dialog.
| Prop | Type | Default |
|---|---|---|
| asChild | boolean | false |
| Change the default rendered element for the one passed as a child. |
Examples
Basic
A simple dialog with title, description, and close button.
<Dialog>
<DialogTrigger>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete your account
and remove your data from our servers.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button variant="destructive">Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>Controlled
Control the dialog open state from your component.
// In your component:
isOpen = signal(false);
// In your template:
<Dialog [open]="isOpen()" (openChange)="isOpen.set($event)">
<DialogTrigger>
<Button>Open Controlled Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Controlled Dialog</DialogTitle>
<DialogDescription>
This dialog state is controlled by the component.
</DialogDescription>
</DialogHeader>
<Button (click)="isOpen.set(false)">Close via Component</Button>
</DialogContent>
</Dialog>
<!-- Open from anywhere -->
<Button (click)="isOpen.set(true)">Programmatically Open</Button>With Form
A dialog with a form inside using reactive forms.
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
// In your component:
form = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
message: ['']
});
constructor(private fb: FormBuilder) {}
onSubmit() {
if (this.form.valid) {
console.log(this.form.value);
// Handle submission
}
}
// In your template:
<Dialog>
<DialogTrigger>
<Button>Contact Us</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Send us a message</DialogTitle>
<DialogDescription>
Fill out the form below and we'll get back to you as soon as possible.
</DialogDescription>
</DialogHeader>
<form [formGroup]="form" class="space-y-4">
<div class="space-y-2">
<Label for="name">Name</Label>
<Input
id="name"
placeholder="Your name"
formControlName="name"
/>
</div>
<div class="space-y-2">
<Label for="email">Email</Label>
<Input
id="email"
type="email"
placeholder="your.email@example.com"
formControlName="email"
/>
</div>
<div class="space-y-2">
<Label for="message">Message</Label>
<Textarea
id="message"
placeholder="Your message here..."
formControlName="message"
/>
</div>
</form>
<DialogFooter>
<DialogClose>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button (click)="onSubmit()" [disabled]="form.invalid">
Send Message
</Button>
</DialogFooter>
</DialogContent>
</Dialog>Alert Dialog
A dialog used for important alerts and confirmations.
<Dialog>
<DialogTrigger>
<Button variant="destructive">Delete Account</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Account</DialogTitle>
<DialogDescription>
Warning: This action is permanent and cannot be reversed.
</DialogDescription>
</DialogHeader>
<div class="space-y-4">
<p class="text-sm text-muted-foreground">
Your account and all associated data will be permanently deleted.
This includes all your projects, files, and settings.
</p>
</div>
<DialogFooter>
<DialogClose>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button
variant="destructive"
(click)="deleteAccount()"
>
Delete My Account
</Button>
</DialogFooter>
</DialogContent>
</Dialog>Nested Dialogs
Open another dialog from within a dialog.
<Dialog>
<DialogTrigger>
<Button>Parent Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Parent Dialog</DialogTitle>
<DialogDescription>
Click below to open a nested dialog.
</DialogDescription>
</DialogHeader>
<!-- Nested dialog -->
<Dialog>
<DialogTrigger>
<Button variant="outline">Open Nested Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Nested Dialog</DialogTitle>
<DialogDescription>
This is a dialog nested within another dialog.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose>
<Button>Close</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<DialogFooter>
<DialogClose>
<Button variant="outline">Close Parent</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>Custom Content Layout
Create a dialog with custom content layout and styling.
<Dialog>
<DialogTrigger>
<Button variant="secondary">Open Custom Dialog</Button>
</DialogTrigger>
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Create New Project</DialogTitle>
<DialogDescription>
Set up a new project with your preferred settings.
</DialogDescription>
</DialogHeader>
<div class="grid grid-cols-2 gap-4 py-4">
<div class="space-y-3">
<h3 class="text-sm font-semibold">Project Settings</h3>
<div class="space-y-2">
<Label for="project-name">Project Name</Label>
<Input id="project-name" placeholder="Enter project name" />
</div>
<div class="space-y-2">
<Label for="project-type">Project Type</Label>
<Select>
<SelectTrigger id="project-type">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="web">Web</SelectItem>
<SelectItem value="mobile">Mobile</SelectItem>
<SelectItem value="desktop">Desktop</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div class="space-y-3">
<h3 class="text-sm font-semibold">Visibility</h3>
<div class="space-y-2">
<div class="flex items-center space-x-2">
<Checkbox id="private" />
<Label for="private" class="font-normal">Private</Label>
</div>
<div class="flex items-center space-x-2">
<Checkbox id="org-access" />
<Label for="org-access" class="font-normal">Org Access</Label>
</div>
</div>
</div>
</div>
<DialogFooter>
<DialogClose>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Create Project</Button>
</DialogFooter>
</DialogContent>
</Dialog>Loading State
Dialog with loading state during async operation.
isLoading = signal(false);
async submitForm() {
this.isLoading.set(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
console.log('Success!');
} finally {
this.isLoading.set(false);
}
}
// In template:
<Dialog>
<DialogTrigger>
<Button>Submit Data</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Submit Data</DialogTitle>
<DialogDescription>
This will send your data to the server.
</DialogDescription>
</DialogHeader>
<div class="space-y-4">
<p>Ready to submit?</p>
</div>
<DialogFooter>
<DialogClose [disabled]="isLoading()">
<Button variant="outline" [disabled]="isLoading()">Cancel</Button>
</DialogClose>
<Button (click)="submitForm()" [disabled]="isLoading()">
@if (isLoading()) {
<Spinner class="mr-2 h-4 w-4 animate-spin" />
Submitting...
} @else {
Submit
}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>Accessibility
Adheres to the Dialog (Modal) WAI-ARIA design pattern .
Keyboard Interactions
| Key | Description |
|---|---|
| Tab | Moves focus to the next focusable element within the dialog. Focus is trapped within the dialog. |
| Shift + Tab | Moves focus to the previous focusable element within the dialog. |
| Escape | Closes the dialog if it is configured to close on Escape. |
| Enter | Activates the focused button or submits the form if a form field is focused. |
| Space | Activates the focused button or toggles the focused checkbox. |