Dialog

A modal dialog that overlays the main content and requires user interaction. Based on Radix UI Dialog with full accessibility support.

Preview Code
Interactive

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.

Angular CLInpmpnpmyarnbun
Bash
ng g @ng-cn/core:c dialog

Anatomy

Import all parts and piece them together.

Typescript
import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose
} from '@/ui/dialog';
Html
<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.

PropTypeDefault
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

AttributeValues
[data-state]"open" | "closed"

Trigger

The button that opens the dialog. This element must be rendered.

PropTypeDefault
asChild boolean false
Change the default rendered element for the one passed as a child, merging their props and behavior.

Data Attributes

AttributeValues
[aria-haspopup]"dialog"
[aria-expanded]true | false
[aria-controls]id of DialogContent

Content

The dialog modal content. Should contain DialogHeader, content, and DialogFooter.

PropTypeDefault
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

AttributeValues
[data-state]"open" | "closed"
[role]"dialog"
[aria-modal]true
[aria-labelledby]id of DialogTitle
[aria-describedby]id of DialogDescription

CSS Variables

VariableDescription
--dialog-animation-durationDuration of entry and exit animations

Header

Container for the dialog title and description.

PropTypeDefault
class string
Additional CSS classes to apply to the header.

Title

The accessible title of the dialog. Automatically connected via ARIA.

PropTypeDefault
class string
Additional CSS classes to apply to the title.

Data Attributes

AttributeValues
[id]auto-generated unique id

Description

Optional accessible description of the dialog.

PropTypeDefault
class string
Additional CSS classes to apply to the description.

Data Attributes

AttributeValues
[id]auto-generated unique id

Footer

Container for dialog action buttons, typically placed at the bottom.

PropTypeDefault
class string
Additional CSS classes to apply to the footer.

Close

A button that closes the dialog. Must be rendered within the dialog.

PropTypeDefault
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.

Html
<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.

Html
// 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.

Html
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.

Html
<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.

Html
<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.

Html
<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.

Html
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

KeyDescription
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.