dialog

Visually or semantically separates content.

PreviousNext
import { ChangeDetectionStrategy, Component, inject, signal, type AfterViewInit } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardDialogImports } from '@/shared/components/dialog/dialog.imports';
import { Z_MODAL_DATA, ZardDialogService } from '@/shared/components/dialog/dialog.service';
import { ZardInputComponent } from '@/shared/components/input/input.component';
import { ZardTabsImports } from '@/shared/components/tabs/tabs.imports';

interface DialogData {
  name: string;
  username: string;
}

/** Content the service renders inside the dialog; reads what was passed through `zData`. */
@Component({
  selector: 'z-demo-dialog-preview-content',
  imports: [ReactiveFormsModule, ZardInputComponent],
  template: `
    <form [formGroup]="form" class="grid gap-4">
      <div class="grid gap-3">
        <label for="dialog-service-name" class="text-sm leading-none font-medium select-none">Name</label>
        <input z-input id="dialog-service-name" formControlName="name" />
      </div>
      <div class="grid gap-3">
        <label for="dialog-service-username" class="text-sm leading-none font-medium select-none">Username</label>
        <input z-input id="dialog-service-username" formControlName="username" />
      </div>
    </form>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  exportAs: 'zardDemoDialogPreviewContent',
})
export class ZardDemoDialogPreviewContentComponent implements AfterViewInit {
  private readonly data = inject(Z_MODAL_DATA) as DialogData;

  readonly form = new FormGroup({
    name: new FormControl('Pedro Duarte'),
    username: new FormControl('@peduarte'),
  });

  ngAfterViewInit(): void {
    if (this.data) {
      this.form.patchValue(this.data);
    }
  }
}

@Component({
  selector: 'z-demo-dialog-preview',
  imports: [ZardButtonComponent, ZardDialogImports, ZardInputComponent, ZardTabsImports, ReactiveFormsModule],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <button type="button" z-button zType="outline" (click)="visible.set(true)">Edit profile</button>

        <z-dialog [(zVisible)]="visible">
          <z-dialog-header>
            <z-dialog-title>Edit profile</z-dialog-title>
            <z-dialog-description>Make changes to your profile here. Click save when you're done.</z-dialog-description>
          </z-dialog-header>
          <form [formGroup]="form" class="grid gap-4">
            <div class="grid gap-3">
              <label for="dialog-name" class="text-sm leading-none font-medium select-none">Name</label>
              <input z-input id="dialog-name" formControlName="name" />
            </div>
            <div class="grid gap-3">
              <label for="dialog-username" class="text-sm leading-none font-medium select-none">Username</label>
              <input z-input id="dialog-username" formControlName="username" />
            </div>
          </form>
          <z-dialog-footer>
            <button type="button" z-button zType="outline" z-dialog-close>Cancel</button>
            <button type="button" z-button (click)="save()">Save changes</button>
          </z-dialog-footer>
        </z-dialog>
      </z-tab>

      <z-tab label="Service">
        <button type="button" z-button zType="outline" (click)="openDialog()">Edit profile</button>
      </z-tab>
    </z-tab-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDialogPreviewComponent {
  private readonly dialogService = inject(ZardDialogService);

  readonly visible = signal(false);

  readonly form = new FormGroup({
    name: new FormControl('Samuel Rizzon'),
    username: new FormControl('@samuelrizzondev'),
  });

  save() {
    console.log('Form submitted:', this.form.value);
    this.visible.set(false);
  }

  openDialog() {
    this.dialogService.create({
      zTitle: 'Edit profile',
      zDescription: `Make changes to your profile here. Click save when you're done.`,
      zContent: ZardDemoDialogPreviewContentComponent,
      zData: {
        name: 'Samuel Rizzon',
        username: '@samuelrizzondev',
      } satisfies DialogData,
      zOkText: 'Save changes',
      zOnOk: instance => {
        console.log('Form submitted:', instance.form.value);
      },
    });
  }
}

Installation

Copy
npx zard-cli@latest add dialog

Usage

import { ZardDialogImports } from '@/shared/components/dialog/dialog.imports';
Copy
<button type="button" z-button zType="outline" (click)="visible.set(true)">Edit profile</button>

<z-dialog [(zVisible)]="visible">
  <z-dialog-header>
    <z-dialog-title>Edit profile</z-dialog-title>
    <z-dialog-description>Make changes to your profile here.</z-dialog-description>
  </z-dialog-header>
  <p>Dialog content goes here.</p>
  <z-dialog-footer>
    <button type="button" z-button zType="outline" z-dialog-close>Cancel</button>
    <button type="button" z-button (click)="save()">Save changes</button>
  </z-dialog-footer>
</z-dialog>
Copy

Composition

Use the following composition to build a dialog:

z-dialog
├── z-dialog-header
│   ├── z-dialog-title
│   └── z-dialog-description
├── (your content)
└── z-dialog-footer
    └── [z-dialog-close]
Copy

Examples

custom close

Replace the default footer with your own: a z-dialog-footer with a [z-dialog-close] button works in both forms, the service one with zHideFooter: true.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardDialogImports } from '@/shared/components/dialog/dialog.imports';
import { ZardDialogService } from '@/shared/components/dialog/dialog.service';
import { ZardInputComponent } from '@/shared/components/input/input.component';
import { ZardTabsImports } from '@/shared/components/tabs/tabs.imports';

/** Content the service renders; it brings its own footer, so the options hide the default one. */
@Component({
  selector: 'z-demo-dialog-custom-close-content',
  imports: [ZardButtonComponent, ZardDialogImports, ZardInputComponent],
  template: `
    <div class="flex items-center gap-2">
      <div class="grid flex-1 gap-2">
        <label for="share-link-service" class="sr-only">Link</label>
        <input z-input id="share-link-service" value="https://ui.zardui.com/docs/installation" readonly />
      </div>
    </div>
    <z-dialog-footer class="sm:justify-start">
      <button type="button" z-button z-dialog-close>Close</button>
    </z-dialog-footer>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDialogCustomCloseContentComponent {}

@Component({
  selector: 'z-demo-dialog-custom-close',
  imports: [ZardButtonComponent, ZardDialogImports, ZardInputComponent, ZardTabsImports],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <button type="button" z-button zType="outline" (click)="visible.set(true)">Share</button>

        <z-dialog [(zVisible)]="visible">
          <z-dialog-header>
            <z-dialog-title>Share link</z-dialog-title>
            <z-dialog-description>Anyone who has this link will be able to view this.</z-dialog-description>
          </z-dialog-header>
          <div class="flex items-center gap-2">
            <div class="grid flex-1 gap-2">
              <label for="share-link" class="sr-only">Link</label>
              <input z-input id="share-link" value="https://ui.zardui.com/docs/installation" readonly />
            </div>
          </div>
          <z-dialog-footer class="sm:justify-start">
            <button type="button" z-button z-dialog-close>Close</button>
          </z-dialog-footer>
        </z-dialog>
      </z-tab>

      <z-tab label="Service">
        <button type="button" z-button zType="outline" (click)="open()">Share</button>
      </z-tab>
    </z-tab-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDialogCustomCloseComponent {
  private readonly dialogService = inject(ZardDialogService);

  readonly visible = signal(false);

  open() {
    this.dialogService.create({
      zTitle: 'Share link',
      zDescription: 'Anyone who has this link will be able to view this.',
      zContent: ZardDemoDialogCustomCloseContentComponent,
      zHideFooter: true,
    });
  }
}

no close button

Set [zClosable]="false" in the template, or zClosable: false in the options, to hide the close button; press Escape or click outside the dialog to close it.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardDialogImports } from '@/shared/components/dialog/dialog.imports';
import { ZardDialogService } from '@/shared/components/dialog/dialog.service';
import { ZardTabsImports } from '@/shared/components/tabs/tabs.imports';

@Component({
  selector: 'z-demo-dialog-no-close-button',
  imports: [ZardButtonComponent, ZardDialogImports, ZardTabsImports],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <button type="button" z-button zType="outline" (click)="visible.set(true)">No Close Button</button>

        <z-dialog [(zVisible)]="visible" [zClosable]="false">
          <z-dialog-header>
            <z-dialog-title>No Close Button</z-dialog-title>
            <z-dialog-description>
              This dialog doesn't have a close button in the top-right corner.
            </z-dialog-description>
          </z-dialog-header>
        </z-dialog>
      </z-tab>

      <z-tab label="Service">
        <button type="button" z-button zType="outline" (click)="open()">No Close Button</button>
      </z-tab>
    </z-tab-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDialogNoCloseButtonComponent {
  private readonly dialogService = inject(ZardDialogService);

  readonly visible = signal(false);

  open() {
    this.dialogService.create({
      zTitle: 'No Close Button',
      zDescription: "This dialog doesn't have a close button in the top-right corner.",
      zClosable: false,
      zHideFooter: true,
    });
  }
}

sticky footer

Keep actions visible while the content scrolls.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardDialogImports } from '@/shared/components/dialog/dialog.imports';
import { ZardDialogService } from '@/shared/components/dialog/dialog.service';
import { ZardTabsImports } from '@/shared/components/tabs/tabs.imports';

const PARAGRAPHS = Array.from({ length: 10 }).map(
  () =>
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
);

/** Content the service renders: the paragraphs scroll while the footer stays put. */
@Component({
  selector: 'z-demo-dialog-sticky-footer-content',
  template: `
    <div class="no-scrollbar -mx-4 max-h-[50vh] overflow-y-auto px-4">
      @for (paragraph of paragraphs; track $index) {
        <p class="mb-4 leading-normal">{{ paragraph }}</p>
      }
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDialogStickyFooterContentComponent {
  protected readonly paragraphs = PARAGRAPHS;
}

@Component({
  selector: 'z-demo-dialog-sticky-footer',
  imports: [ZardButtonComponent, ZardDialogImports, ZardTabsImports],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <button type="button" z-button zType="outline" (click)="visible.set(true)">Sticky Footer</button>

        <z-dialog [(zVisible)]="visible">
          <z-dialog-header>
            <z-dialog-title>Sticky Footer</z-dialog-title>
            <z-dialog-description>
              This dialog has a sticky footer that stays visible while the content scrolls.
            </z-dialog-description>
          </z-dialog-header>
          <div class="no-scrollbar -mx-4 max-h-[50vh] overflow-y-auto px-4">
            @for (paragraph of paragraphs; track $index) {
              <p class="mb-4 leading-normal">{{ paragraph }}</p>
            }
          </div>
          <z-dialog-footer>
            <button type="button" z-button zType="outline" z-dialog-close>Close</button>
          </z-dialog-footer>
        </z-dialog>
      </z-tab>

      <z-tab label="Service">
        <button type="button" z-button zType="outline" (click)="open()">Sticky Footer</button>
      </z-tab>
    </z-tab-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDialogStickyFooterComponent {
  private readonly dialogService = inject(ZardDialogService);

  protected readonly paragraphs = PARAGRAPHS;
  readonly visible = signal(false);

  open() {
    this.dialogService.create({
      zTitle: 'Sticky Footer',
      zDescription: 'This dialog has a sticky footer that stays visible while the content scrolls.',
      zContent: ZardDemoDialogStickyFooterContentComponent,
      zCancelText: 'Close',
      zOkText: null,
    });
  }
}

scrollable content

Long content can scroll while the header stays in view.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardDialogImports } from '@/shared/components/dialog/dialog.imports';
import { ZardDialogService } from '@/shared/components/dialog/dialog.service';
import { ZardTabsImports } from '@/shared/components/tabs/tabs.imports';

const PARAGRAPHS = Array.from({ length: 10 }).map(
  () =>
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
);

/** Content the service renders: the paragraphs scroll inside the dialog. */
@Component({
  selector: 'z-demo-dialog-scrollable-content-content',
  template: `
    <div class="no-scrollbar -mx-4 max-h-[50vh] overflow-y-auto px-4">
      @for (paragraph of paragraphs; track $index) {
        <p class="mb-4 leading-normal">{{ paragraph }}</p>
      }
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDialogScrollableContentInnerComponent {
  protected readonly paragraphs = PARAGRAPHS;
}

@Component({
  selector: 'z-demo-dialog-scrollable-content',
  imports: [ZardButtonComponent, ZardDialogImports, ZardTabsImports],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <button type="button" z-button zType="outline" (click)="visible.set(true)">Scrollable Content</button>

        <z-dialog [(zVisible)]="visible">
          <z-dialog-header>
            <z-dialog-title>Scrollable Content</z-dialog-title>
            <z-dialog-description>This is a dialog with scrollable content.</z-dialog-description>
          </z-dialog-header>
          <div class="no-scrollbar -mx-4 max-h-[50vh] overflow-y-auto px-4">
            @for (paragraph of paragraphs; track $index) {
              <p class="mb-4 leading-normal">{{ paragraph }}</p>
            }
          </div>
        </z-dialog>
      </z-tab>

      <z-tab label="Service">
        <button type="button" z-button zType="outline" (click)="open()">Scrollable Content</button>
      </z-tab>
    </z-tab-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDialogScrollableContentComponent {
  private readonly dialogService = inject(ZardDialogService);

  protected readonly paragraphs = PARAGRAPHS;
  readonly visible = signal(false);

  open() {
    this.dialogService.create({
      zTitle: 'Scrollable Content',
      zDescription: 'This is a dialog with scrollable content.',
      zContent: ZardDemoDialogScrollableContentInnerComponent,
      zHideFooter: true,
    });
  }
}

rtl

To enable RTL support in zard/ui, see the RTL configuration guide.
// You will notice this example is rendered inside a wrapper that sets the dir and data-lang attributes, and
// reads its text from a translations helper. This is because this site is not RTL by default.
// In your application you won't need either: set dir="rtl" on <html> (or use the CDK Dir directive on a subtree)
// and write the text of your own language. See the RTL guide: https://zardui.com/docs/rtl

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardDialogImports } from '@/shared/components/dialog/dialog.imports';
import { ZardInputComponent } from '@/shared/components/input/input.component';
import { injectRtlTranslation, type ZardRtlTranslations } from '@/shared/demo-utils/rtl-translation';

interface DialogRtlStrings {
  openDialog: string;
  editProfile: string;
  description: string;
  name: string;
  username: string;
  cancel: string;
  saveChanges: string;
}

const translations: ZardRtlTranslations<DialogRtlStrings> = {
  en: {
    dir: 'ltr',
    values: {
      openDialog: 'Open Dialog',
      editProfile: 'Edit profile',
      description: "Make changes to your profile here. Click save when you're done.",
      name: 'Name',
      username: 'Username',
      cancel: 'Cancel',
      saveChanges: 'Save changes',
    },
  },
  ar: {
    dir: 'rtl',
    values: {
      openDialog: 'فتح الحوار',
      editProfile: 'تعديل الملف الشخصي',
      description: 'قم بإجراء تغييرات على ملفك الشخصي هنا. انقر فوق حفظ عند الانتهاء.',
      name: 'الاسم',
      username: 'اسم المستخدم',
      cancel: 'إلغاء',
      saveChanges: 'حفظ التغييرات',
    },
  },
  he: {
    dir: 'rtl',
    values: {
      openDialog: 'פתח דיאלוג',
      editProfile: 'ערוך פרופיל',
      description: 'בצע שינויים בפרופיל שלך כאן. לחץ על שמור כשתסיים.',
      name: 'שם',
      username: 'שם משתמש',
      cancel: 'בטל',
      saveChanges: 'שמור שינויים',
    },
  },
};

@Component({
  selector: 'z-demo-dialog-rtl',
  imports: [ZardButtonComponent, ZardDialogImports, ZardInputComponent],
  template: `
    @let t = i18n.t();
    <button type="button" z-button zType="outline" (click)="visible.set(true)">{{ t.openDialog }}</button>

    <z-dialog [(zVisible)]="visible">
      <z-dialog-header>
        <z-dialog-title>{{ t.editProfile }}</z-dialog-title>
        <z-dialog-description>{{ t.description }}</z-dialog-description>
      </z-dialog-header>

      <div class="grid gap-4">
        <div class="grid gap-3">
          <label for="dialog-rtl-name" class="text-sm leading-none font-medium select-none">{{ t.name }}</label>
          <input z-input id="dialog-rtl-name" value="Pedro Duarte" />
        </div>
        <div class="grid gap-3">
          <label for="dialog-rtl-username" class="text-sm leading-none font-medium select-none">
            {{ t.username }}
          </label>
          <input z-input id="dialog-rtl-username" value="&#64;peduarte" />
        </div>
      </div>

      <z-dialog-footer>
        <button type="button" z-button zType="outline" z-dialog-close>{{ t.cancel }}</button>
        <button type="button" z-button (click)="visible.set(false)">{{ t.saveChanges }}</button>
      </z-dialog-footer>
    </z-dialog>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDialogRtlComponent {
  protected readonly i18n = injectRtlTranslation(translations);

  protected readonly visible = signal(false);
}

API Reference

z-dialogComponent

Root of a declarative dialog. Holds the open state and projects its content into the panel, which exposes `data-state` (`open` | `closed`) while the enter and leave transitions run. The overlay takes the direction of where the dialog is declared: in RTL the close button moves to the left corner and the footer buttons mirror, while the panel stays centered.

PropertyDescriptionTypeDefault
[zVisible] Open state, two-way bound boolean false
[zClosable] Renders the close (X) button in the top-right corner boolean true
[zMaskClosable] Whether clicking outside the dialog (on the mask) closes it boolean true
[zWidth] Custom width (e.g., '400px', '50%') string -
[zDuration] How long the enter and leave transitions run, in ms number 100
[class] Custom CSS classes applied to the panel ClassValue -
(zAfterOpen) Emitted once the dialog is attached EventEmitter<void> -
(zAfterClose) Emitted once the exit transition has finished EventEmitter<void> -

z-dialog-headerComponent

Layout slot for the top of a dialog, next to `z-dialog-title` and `z-dialog-description`.

PropertyDescriptionTypeDefault
[class] Custom CSS classes to apply ClassValue -

z-dialog-footerComponent

Layout slot for the bottom of a dialog, typically the action buttons.

PropertyDescriptionTypeDefault
[class] Custom CSS classes to apply ClassValue -

z-dialog-titleComponent

Accessible name of the dialog. Wired to `aria-labelledby` automatically.

PropertyDescriptionTypeDefault
[zTitle] Title text or template, when not projecting content string | TemplateRef<void> -
[class] Custom CSS classes to apply ClassValue -

z-dialog-descriptionComponent

Supporting text. Wired to `aria-describedby` automatically.

PropertyDescriptionTypeDefault
[zDescription] Description text or template, when not projecting content string | TemplateRef<void> -
[class] Custom CSS classes to apply ClassValue -

[z-dialog-close]Component

Closes the dialog it is projected into. Works for declarative and service-opened dialogs alike.

ZardDialogServiceComponent

Opens the same dialog from code. `create()` takes the options below and returns a `ZardDialogRef`.

PropertyDescriptionTypeDefault
[zCancelIcon] Sets the cancel icon. string -
[zCancelText] Cancel button text, null to hide the button. Defaults to 'Cancel'. string | null 'Cancel'
[zClosable] Whether the close (X) button in the header is shown. Escape and clicking outside the mask still dismiss the dialog unless `zMaskClosable` is also set to false. boolean true
[zContent] Custom content component, template, or HTML. string | TemplateRef<T> | Type<T> -
[zCustomClasses] Additional CSS classes to apply to the dialog panel. ClassValue -
[zData] Data to pass to custom content components. U -
[zDescription] Sets the dialog description. string -
[zDuration] Animation duration (ms) used when closing. Matches the CSS transition. number 100
[zHideFooter] Hides the footer. boolean false
[zHideHeader] Keeps the title and description available to screen readers only (`sr-only`). boolean false
[zMaskClosable] Whether clicking outside the dialog (on the mask) closes it. boolean true
[zOkDestructive] Whether the OK button should have destructive styling. boolean false
[zOkDisabled] Whether the OK button should be disabled. boolean false
[zOkIcon] Sets the OK button icon. string -
[zOkText] OK button text, null to hide the button. Defaults to 'OK'. string | null 'OK'
[zOnCancel] Cancel button click handler. EventEmitter<T> | OnClickCallback<T> -
[zOnOk] OK button click handler. EventEmitter<T> | OnClickCallback<T> -
[zTitle] Dialog title text or template. string | TemplateRef<void> -
[zViewContainerRef] View container reference for dynamic component loading. ViewContainerRef -
[zWidth] Custom width (e.g., '400px', '50%'). string -
github iconwhatsapp icondiscord iconX icon

Made with ❤ in Brazil. Open source and available on GitHub .