popover

Displays rich content in a portal, triggered by a button.

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

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardInputComponent } from '@/shared/components/input/input.component';
import { ZardPopoverImports } from '@/shared/components/popover/popover.imports';

@Component({
  selector: 'z-demo-popover-preview',
  imports: [ZardButtonComponent, ZardInputComponent, ...ZardPopoverImports],
  template: `
    <button type="button" z-button zPopover zType="outline" [zContent]="popoverContent">Open popover</button>

    <ng-template #popoverContent>
      <z-popover class="w-80">
        <div z-popover-header>
          <h4 z-popover-title>Dimensions</h4>
          <p z-popover-description>Set the dimensions for the layer.</p>
        </div>

        <div class="grid gap-2">
          @for (dimension of dimensions; track dimension.id) {
            <div class="grid grid-cols-3 items-center gap-4">
              <label class="text-sm" [attr.for]="dimension.id">{{ dimension.label }}</label>
              <input
                z-input
                type="text"
                class="col-span-2 h-8"
                [id]="dimension.id"
                [value]="dimension.value"
                [attr.aria-label]="dimension.label"
              />
            </div>
          }
        </div>
      </z-popover>
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoPopoverPreviewComponent {
  readonly dimensions = [
    { id: 'width', label: 'Width', value: '100%' },
    { id: 'maxWidth', label: 'Max. width', value: '300px' },
    { id: 'height', label: 'Height', value: '25px' },
    { id: 'maxHeight', label: 'Max. height', value: 'none' },
  ];
}

Installation

Copy
npx zard-cli@latest add popover

Usage

import { ZardPopoverImports } from '@/shared/components/popover/popover.imports';
Copy
<button z-button zType="outline" zPopover [zContent]="popoverContent">Open popover</button>
<ng-template #popoverContent>
  <z-popover>
    <div z-popover-header>
      <h4 z-popover-title>Title</h4>
      <p z-popover-description>Description text here.</p>
    </div>
  </z-popover>
</ng-template>
Copy

Composition

Use the following composition to build a popover:

button[zPopover]
└── ng-template
    └── z-popover
        └── div[z-popover-header]
            ├── h4[z-popover-title]
            └── p[z-popover-description]
Copy

Examples

basic

A popover with a header, a title and a description, aligned to the start of the trigger.
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardPopoverImports } from '@/shared/components/popover/popover.imports';

@Component({
  selector: 'z-demo-popover-basic',
  imports: [ZardButtonComponent, ...ZardPopoverImports],
  template: `
    <button type="button" z-button zPopover zAlign="start" zType="outline" class="w-fit" [zContent]="popoverContent">
      Open Popover
    </button>

    <ng-template #popoverContent>
      <z-popover>
        <div z-popover-header>
          <h4 z-popover-title>Dimensions</h4>
          <p z-popover-description>Set the dimensions for the layer.</p>
        </div>
      </z-popover>
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoPopoverBasicComponent {}

align

Set zAlign to start, center or end to align the popover along the side chosen by zPlacement.
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardPopoverImports } from '@/shared/components/popover/popover.imports';

@Component({
  selector: 'z-demo-popover-align',
  imports: [ZardButtonComponent, ...ZardPopoverImports],
  template: `
    <div class="flex gap-6">
      @for (alignment of alignments; track alignment.align) {
        <button
          type="button"
          z-button
          zPopover
          zSize="sm"
          zType="outline"
          [zAlign]="alignment.align"
          [zContent]="popoverContent"
        >
          {{ alignment.label }}
        </button>

        <ng-template #popoverContent>
          <z-popover class="w-40">{{ alignment.content }}</z-popover>
        </ng-template>
      }
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoPopoverAlignComponent {
  readonly alignments = [
    { align: 'start', label: 'Start', content: 'Aligned to start' },
    { align: 'center', label: 'Center', content: 'Aligned to center' },
    { align: 'end', label: 'End', content: 'Aligned to end' },
  ] as const;
}

form

A popover holding a form built with the z-field components.
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardFieldImports } from '@/shared/components/field/field.imports';
import { ZardInputComponent } from '@/shared/components/input/input.component';
import { ZardPopoverImports } from '@/shared/components/popover/popover.imports';

@Component({
  selector: 'z-demo-popover-form',
  imports: [ZardButtonComponent, ZardInputComponent, ...ZardFieldImports, ...ZardPopoverImports],
  template: `
    <button type="button" z-button zPopover zAlign="start" zType="outline" [zContent]="popoverContent">
      Open Popover
    </button>

    <ng-template #popoverContent>
      <z-popover class="w-64">
        <div z-popover-header>
          <h4 z-popover-title>Dimensions</h4>
          <p z-popover-description>Set the dimensions for the layer.</p>
        </div>

        <div z-field-group class="gap-4">
          <div z-field zOrientation="horizontal">
            <label z-field-label for="form-width" class="w-1/2">Width</label>
            <input z-input type="text" id="form-width" value="100%" />
          </div>

          <div z-field zOrientation="horizontal">
            <label z-field-label for="form-height" class="w-1/2">Height</label>
            <input z-input type="text" id="form-height" value="25px" />
          </div>
        </div>
      </z-popover>
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoPopoverFormComponent {}

placement

Set zPlacement to top, bottom, left or right to choose which side of the trigger the popover opens on. The overlay flips to another side automatically when the preferred placement does not fit in the viewport.
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardPopoverImports } from '@/shared/components/popover/popover.imports';

@Component({
  selector: 'z-demo-popover-placement',
  imports: [ZardButtonComponent, ...ZardPopoverImports],
  template: `
    <div class="flex flex-col space-y-2">
      <button type="button" z-button zPopover zPlacement="top" zType="outline" [zContent]="popoverContent">Top</button>

      <div class="flex space-x-2">
        <button type="button" z-button zPopover zPlacement="left" zType="outline" [zContent]="popoverContent">
          Left
        </button>
        <button type="button" z-button zPopover zPlacement="right" zType="outline" [zContent]="popoverContent">
          Right
        </button>
      </div>

      <button type="button" z-button zPopover zPlacement="bottom" zType="outline" [zContent]="popoverContent">
        Bottom
      </button>
    </div>

    <ng-template #popoverContent>
      <z-popover class="w-64">
        <div z-popover-header>
          <h4 z-popover-title>Placement</h4>
          <p z-popover-description>The popover flips automatically when it does not fit.</p>
        </div>
      </z-popover>
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoPopoverPlacementComponent {}

hover

Set zTrigger="hover" to open the popover on pointer enter instead of click. Prefer [zHoverCard] for a hover-triggered preview of non-essential content — it adds open/close delays that a hover-triggered popover does not have, so a passing cursor does not trigger it.
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardPopoverImports } from '@/shared/components/popover/popover.imports';

@Component({
  selector: 'z-demo-popover-hover',
  imports: [ZardButtonComponent, ...ZardPopoverImports],
  template: `
    <button type="button" z-button zPopover zTrigger="hover" zType="outline" [zContent]="popoverContent">
      Hover me
    </button>

    <ng-template #popoverContent>
      <z-popover>
        <div z-popover-header>
          <h4 z-popover-title>Hover content</h4>
          <p z-popover-description>This popover appears when you hover over the button.</p>
        </div>
      </z-popover>
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoPopoverHoverComponent {}

close on action

Read the zPopover directive with viewChild and call hide() to close the popover programmatically — for example, after an in-panel action like a save button completes.
import { ChangeDetectionStrategy, Component, signal, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardFieldImports } from '@/shared/components/field/field.imports';
import { ZardInputComponent } from '@/shared/components/input/input.component';
import { ZardPopoverDirective } from '@/shared/components/popover/popover.component';
import { ZardPopoverImports } from '@/shared/components/popover/popover.imports';

@Component({
  selector: 'z-demo-popover-close-on-action',
  imports: [FormsModule, ZardButtonComponent, ZardInputComponent, ...ZardFieldImports, ...ZardPopoverImports],
  template: `
    <button type="button" z-button zPopover zType="outline" [zContent]="popoverContent" #popoverTrigger>
      Settings
    </button>

    <ng-template #popoverContent>
      <z-popover>
        <div z-popover-header>
          <h4 z-popover-title>Settings</h4>
          <p z-popover-description>Manage your account settings.</p>
        </div>

        <div z-field-group class="gap-3">
          <div z-field>
            <label z-field-label for="close-on-action-width">Width</label>
            <input id="close-on-action-width" z-input type="text" placeholder="100%" [(ngModel)]="width" />
          </div>

          <div z-field>
            <label z-field-label for="close-on-action-height">Height</label>
            <input id="close-on-action-height" z-input type="text" placeholder="25px" [(ngModel)]="height" />
          </div>
        </div>

        <button type="button" z-button class="w-full" zSize="sm" (click)="saveChanges()">Save changes</button>
      </z-popover>
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoPopoverCloseOnActionComponent {
  readonly popoverDirective = viewChild.required('popoverTrigger', { read: ZardPopoverDirective });

  readonly width = signal('100%');
  readonly height = signal('25px');

  saveChanges() {
    this.popoverDirective().hide();
  }
}

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 } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import type { ZardPopoverPlacement } from '@/shared/components/popover/popover.component';
import { ZardPopoverImports } from '@/shared/components/popover/popover.imports';
import { injectRtlTranslation, type ZardRtlTranslations } from '@/shared/demo-utils/rtl-translation';

interface PopoverRtlValues {
  title: string;
  description: string;
  'inline-start': string;
  left: string;
  top: string;
  bottom: string;
  right: string;
  'inline-end': string;
}

const translations: ZardRtlTranslations<PopoverRtlValues> = {
  en: {
    dir: 'ltr',
    values: {
      title: 'Dimensions',
      description: 'Set the dimensions for the layer.',
      'inline-start': 'Inline Start',
      left: 'Left',
      top: 'Top',
      bottom: 'Bottom',
      right: 'Right',
      'inline-end': 'Inline End',
    },
  },
  ar: {
    dir: 'rtl',
    values: {
      title: 'الأبعاد',
      description: 'تعيين الأبعاد للطبقة.',
      'inline-start': 'بداية السطر',
      left: 'يسار',
      top: 'أعلى',
      bottom: 'أسفل',
      right: 'يمين',
      'inline-end': 'نهاية السطر',
    },
  },
  he: {
    dir: 'rtl',
    values: {
      title: 'מימדים',
      description: 'הגדר את המימדים לשכבה.',
      'inline-start': 'תחילת השורה',
      left: 'שמאל',
      top: 'למעלה',
      bottom: 'למטה',
      right: 'ימין',
      'inline-end': 'סוף השורה',
    },
  },
};

@Component({
  selector: 'z-demo-popover-rtl',
  imports: [ZardButtonComponent, ...ZardPopoverImports],
  template: `
    <div class="grid gap-4">
      <div class="flex flex-wrap justify-center gap-2">
        @for (side of physicalSides; track side) {
          <button type="button" z-button zPopover zType="outline" [zPlacement]="side" [zContent]="content">
            {{ i18n.t()[side] }}
          </button>
        }
      </div>
      <div class="flex flex-wrap justify-center gap-2">
        @for (side of logicalSides; track side) {
          <button type="button" z-button zPopover zType="outline" [zPlacement]="side" [zContent]="content">
            {{ i18n.t()[side] }}
          </button>
        }
      </div>
    </div>

    <ng-template #content>
      <z-popover>
        <div z-popover-header>
          <h4 z-popover-title>{{ i18n.t().title }}</h4>
          <p z-popover-description>{{ i18n.t().description }}</p>
        </div>
      </z-popover>
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoPopoverRtlComponent {
  protected readonly i18n = injectRtlTranslation(translations);

  protected readonly physicalSides: ZardPopoverPlacement[] = ['left', 'top', 'bottom', 'right'];
  protected readonly logicalSides: ZardPopoverPlacement[] = ['inline-start', 'inline-end'];
}

API Reference

[zPopover]Component

The directive that creates a popover when applied to a trigger element.

PropertyDescriptionTypeDefault
[zTrigger] How the popover is triggered 'click' | 'hover' | null 'click'
[zContent] Required. Template to display in the popover TemplateRef<unknown> -
[zPlacement] Side of the trigger the popover opens on. `inline-start` and `inline-end` follow the text direction 'top' | 'bottom' | 'left' | 'right' | 'inline-start' | 'inline-end' 'bottom'
[zAlign] Alignment of the popover along the side of the trigger 'start' | 'center' | 'end' 'center'
[zSideOffset] Distance in pixels between the popover and the trigger number 4
[zAlignOffset] Offset in pixels along the alignment axis number 0
[zOrigin] Custom anchor element ElementRef -
[zVisible] Control visibility programmatically boolean false
[zOverlayClickable] Close on outside click boolean true
(zVisibleChange) Emits when visibility changes. Fires immediately, before the exit animation ends EventEmitter<boolean> -

z-popoverComponent

The popover content. Exposes `data-side`, `data-align` and `data-open`/`data-closed` while it is mounted.

PropertyDescriptionTypeDefault
[class] Additional CSS classes ClassValue ''

z-popover-headerComponent

Groups the title and the description at the top of the popover.

PropertyDescriptionTypeDefault
[class] Additional CSS classes ClassValue ''

z-popover-titleComponent

The popover title. Wired to the content through `aria-labelledby`.

PropertyDescriptionTypeDefault
[class] Additional CSS classes ClassValue ''

z-popover-descriptionComponent

The popover description. Wired to the content through `aria-describedby`.

PropertyDescriptionTypeDefault
[class] Additional CSS classes ClassValue ''
github iconwhatsapp icondiscord iconX icon

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