drawer

A draggable panel that slides in from an edge of the screen.

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

import { ZardBadgeComponent } from '@/shared/components/badge';
import { ZardButtonComponent } from '@/shared/components/button';
import { ZardDrawerImports } from '@/shared/components/drawer/drawer.imports';
import { ZardDrawerService } from '@/shared/components/drawer/drawer.service';
import type { ZardDrawerPlacement } from '@/shared/components/drawer/drawer.variants';
import { ZardRadioGroupImports } from '@/shared/components/radio-group/radio-group.imports';
import { ZardTabsImports } from '@/shared/components/tabs/tabs.imports';

import { injectIsMobile } from './support/is-mobile';

const DELIVERY_TIMES = [
  {
    value: 'asap',
    id: 'delivery-asap',
    label: 'Standard delivery',
    description: '25–35 min · Driver assigned now',
    badge: 'Fastest',
  },
  { value: '5-00', id: 'delivery-5-00', label: '5:00 PM – 5:15 PM', description: 'Prep starts at 4:45 PM' },
  { value: '5-30', id: 'delivery-5-30', label: '5:30 PM – 5:45 PM', description: `Good if you're heading home` },
  { value: '6-00', id: 'delivery-6-00', label: '6:00 PM – 6:15 PM', description: 'Most popular · High demand' },
  { value: '6-30', id: 'delivery-6-30', label: '6:30 PM – 6:45 PM', description: 'Last slot before kitchen closes' },
];

/** Content the service renders: the same list, so the pick is read back from the instance in `zOnOk`. */
@Component({
  selector: 'z-demo-drawer-preview-content',
  imports: [ZardBadgeComponent, ...ZardRadioGroupImports],
  template: `
    <z-radio-group [(value)]="deliveryTime" class="grid w-full gap-2">
      @for (time of times; track time.value) {
        <label
          [for]="'service-' + time.id"
          class="has-data-[state=checked]:bg-input/30 flex w-full items-center gap-3 rounded-2xl border p-4 select-none"
        >
          <span class="flex flex-1 flex-col gap-1 leading-snug">
            <span class="flex items-center gap-2 text-sm font-medium">
              {{ time.label }}
              @if (time.badge) {
                <z-badge zType="secondary">{{ time.badge }}</z-badge>
              }
            </span>
            <span class="text-muted-foreground text-sm">{{ time.description }}</span>
          </span>
          <z-radio [zId]="'service-' + time.id" [value]="time.value" />
        </label>
      }
    </z-radio-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDrawerPreviewContentComponent {
  readonly times = DELIVERY_TIMES;
  readonly deliveryTime = signal<unknown>('asap');
}

@Component({
  selector: 'z-demo-drawer-preview',
  imports: [ZardBadgeComponent, ZardButtonComponent, ZardDrawerImports, ...ZardRadioGroupImports, ZardTabsImports],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <button type="button" z-button zType="secondary" (click)="visible.set(true)">Open Drawer</button>

        <z-drawer [(zVisible)]="visible" [zPlacement]="placement()" [zHandle]="isMobile()">
          <z-drawer-header>
            <z-drawer-title>Pick a delivery time</z-drawer-title>
            <z-drawer-description>We'll prepare your order as soon as possible.</z-drawer-description>
          </z-drawer-header>
          <div class="flex-1 overflow-y-auto p-4">
            <z-radio-group [(value)]="deliveryTime" class="grid w-full gap-2">
              @for (time of times; track time.value) {
                <label
                  [for]="time.id"
                  class="has-data-[state=checked]:bg-input/30 flex w-full items-center gap-3 rounded-2xl border p-4 select-none"
                >
                  <span class="flex flex-1 flex-col gap-1 leading-snug">
                    <span class="flex items-center gap-2 text-sm font-medium">
                      {{ time.label }}
                      @if (time.badge) {
                        <z-badge zType="secondary">{{ time.badge }}</z-badge>
                      }
                    </span>
                    <span class="text-muted-foreground text-sm">{{ time.description }}</span>
                  </span>
                  <z-radio [zId]="time.id" [value]="time.value" />
                </label>
              }
            </z-radio-group>
          </div>
          <z-drawer-footer>
            <button type="button" z-button (click)="visible.set(false)">Confirm Delivery Time</button>
            <button type="button" z-button zType="ghost" z-drawer-close>Cancel</button>
          </z-drawer-footer>
        </z-drawer>
      </z-tab>

      <z-tab label="Service">
        <button type="button" z-button zType="secondary" (click)="openDrawer()">Open Drawer</button>
      </z-tab>
    </z-tab-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDrawerPreviewComponent {
  private readonly drawerService = inject(ZardDrawerService);
  private readonly isMobileViewport = injectIsMobile();

  readonly times = DELIVERY_TIMES;
  readonly visible = signal(false);
  readonly deliveryTime = signal<unknown>('asap');

  /** Bottom sheet where the screen is narrow, side panel where there is room. */
  readonly isMobile = computed(() => this.isMobileViewport());
  readonly placement = computed<ZardDrawerPlacement>(() => (this.isMobile() ? 'bottom' : 'right'));

  openDrawer() {
    this.drawerService.create({
      zTitle: 'Pick a delivery time',
      zDescription: `We'll prepare your order as soon as possible.`,
      zContent: ZardDemoDrawerPreviewContentComponent,
      zPlacement: this.placement(),
      zHandle: this.isMobile(),
      zOkText: 'Confirm Delivery Time',
      zCancelText: 'Cancel',
      zOnOk: instance => {
        console.log('Delivery time:', instance.deliveryTime());
      },
    });
  }
}

Installation

Copy
npx zard-cli@latest add drawer

Usage

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

<z-drawer [(zVisible)]="visible">
  <z-drawer-header>
    <z-drawer-title>Are you absolutely sure?</z-drawer-title>
    <z-drawer-description>This action cannot be undone.</z-drawer-description>
  </z-drawer-header>

  <div class="p-4"><!-- Content here --></div>

  <z-drawer-footer>
    <button type="button" z-button>Submit</button>
    <button type="button" z-button zType="outline" z-drawer-close>Cancel</button>
  </z-drawer-footer>
</z-drawer>
Copy

Composition

Use the following composition to build a drawer:

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

Examples

custom sizes

A vertical drawer sizes itself to its content and is capped at calc(100dvh - 6rem). A side drawer spans 75% of the viewport width, or 24rem on larger screens. Override either with class.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button';
import { ZardDrawerImports } from '@/shared/components/drawer/drawer.imports';
import { ZardDrawerService } from '@/shared/components/drawer/drawer.service';
import { ZardTabsImports } from '@/shared/components/tabs/tabs.imports';

/** Placeholder block the service renders as string content. */
const TALL_BOX = '<div class="bg-muted h-96 w-full rounded-2xl"></div>';
const FILL_BOX = '<div class="bg-muted min-h-40 w-full rounded-2xl"></div>';

@Component({
  selector: 'z-demo-drawer-custom-sizes',
  imports: [ZardButtonComponent, ZardDrawerImports, ZardTabsImports],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <div class="flex flex-wrap gap-2">
          <button type="button" z-button zType="secondary" (click)="halfHeight.set(true)">Half height</button>
          <button type="button" z-button zType="secondary" (click)="wideSide.set(true)">Wide side</button>
        </div>

        <z-drawer [(zVisible)]="halfHeight" class="h-[50vh]">
          <z-drawer-header>
            <z-drawer-title>Half height</z-drawer-title>
            <z-drawer-description>The drawer keeps the height you give it.</z-drawer-description>
          </z-drawer-header>
          <div class="flex-1 overflow-y-auto p-4">
            <div class="bg-muted h-96 w-full rounded-2xl"></div>
          </div>
        </z-drawer>

        <z-drawer [(zVisible)]="wideSide" zPlacement="right" class="sm:w-lg">
          <z-drawer-header>
            <z-drawer-title>Wide side</z-drawer-title>
            <z-drawer-description>A side drawer is 24rem wide until you widen it.</z-drawer-description>
          </z-drawer-header>
          <div class="flex-1 p-4">
            <div class="bg-muted size-full rounded-2xl"></div>
          </div>
        </z-drawer>
      </z-tab>

      <z-tab label="Service">
        <div class="flex flex-wrap gap-2">
          <button type="button" z-button zType="secondary" (click)="openHalfHeight()">Half height</button>
          <button type="button" z-button zType="secondary" (click)="openWideSide()">Wide side</button>
        </div>
      </z-tab>
    </z-tab-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDrawerCustomSizesComponent {
  private readonly drawerService = inject(ZardDrawerService);

  readonly halfHeight = signal(false);
  readonly wideSide = signal(false);

  openHalfHeight() {
    this.drawerService.create({
      zTitle: 'Half height',
      zDescription: 'The drawer keeps the height you give it.',
      zCustomClasses: 'h-[50vh]',
      zContent: TALL_BOX,
      zHideFooter: true,
    });
  }

  openWideSide() {
    this.drawerService.create({
      zTitle: 'Wide side',
      zDescription: 'A side drawer is 24rem wide until you widen it.',
      zPlacement: 'right',
      zCustomClasses: 'sm:w-lg',
      zContent: FILL_BOX,
      zHideFooter: true,
    });
  }
}

position

Use zPlacement to set the side of the drawer. Values are top, right, bottom and left.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button';
import { ZardDrawerImports } from '@/shared/components/drawer/drawer.imports';
import { ZardDrawerService } from '@/shared/components/drawer/drawer.service';
import type { ZardDrawerPlacement } from '@/shared/components/drawer/drawer.variants';
import { ZardTabsImports } from '@/shared/components/tabs/tabs.imports';

const PLACEMENTS: ZardDrawerPlacement[] = ['top', 'right', 'bottom', 'left'];

/** Placeholder block the service renders as string content. */
const BOX = '<div class="bg-muted min-h-40 w-full rounded-2xl"></div>';

@Component({
  selector: 'z-demo-drawer-position',
  imports: [ZardButtonComponent, ZardDrawerImports, ZardTabsImports],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <div class="flex flex-wrap gap-2">
          @for (option of placements; track option) {
            <button type="button" z-button zType="secondary" class="capitalize" (click)="open(option)">
              {{ option }}
            </button>
          }
        </div>

        <z-drawer [(zVisible)]="visible" [zPlacement]="placement()">
          <z-drawer-header>
            <z-drawer-title>Move Goal</z-drawer-title>
            <z-drawer-description>Set your daily activity goal.</z-drawer-description>
          </z-drawer-header>
          <div class="flex-1 p-4">
            <div class="bg-muted size-full min-h-40 rounded-2xl"></div>
          </div>
          <z-drawer-footer>
            <button type="button" z-button z-drawer-close>Close</button>
          </z-drawer-footer>
        </z-drawer>
      </z-tab>

      <z-tab label="Service">
        <div class="flex flex-wrap gap-2">
          @for (option of placements; track option) {
            <button type="button" z-button zType="secondary" class="capitalize" (click)="openDrawer(option)">
              {{ option }}
            </button>
          }
        </div>
      </z-tab>
    </z-tab-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDrawerPositionComponent {
  private readonly drawerService = inject(ZardDrawerService);

  readonly placements = PLACEMENTS;
  readonly visible = signal(false);
  readonly placement = signal<ZardDrawerPlacement>('bottom');

  open(placement: ZardDrawerPlacement) {
    this.placement.set(placement);
    this.visible.set(true);
  }

  openDrawer(placement: ZardDrawerPlacement) {
    this.drawerService.create({
      zTitle: 'Move Goal',
      zDescription: 'Set your daily activity goal.',
      zPlacement: placement,
      zContent: BOX,
      zOkText: null,
      zCancelText: 'Close',
    });
  }
}

swipe handle

Use zHandle to render a swipe handle.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button';
import { ZardDrawerImports } from '@/shared/components/drawer/drawer.imports';
import { ZardDrawerService } from '@/shared/components/drawer/drawer.service';
import { ZardTabsImports } from '@/shared/components/tabs/tabs.imports';

/** Placeholder block the service renders as string content. */
const BOX = '<div class="bg-muted h-80 w-full rounded-2xl"></div>';

@Component({
  selector: 'z-demo-drawer-swipe-handle',
  imports: [ZardButtonComponent, ZardDrawerImports, ZardTabsImports],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <button type="button" z-button zType="secondary" (click)="visible.set(true)">Open Drawer</button>

        <z-drawer [(zVisible)]="visible" zHandle>
          <z-drawer-header>
            <z-drawer-title>Drawer</z-drawer-title>
            <z-drawer-description>Drawer with a swipe handle.</z-drawer-description>
          </z-drawer-header>
          <div class="flex-1 p-4">
            <div class="bg-muted h-80 w-full rounded-2xl"></div>
          </div>
          <z-drawer-footer>
            <button type="button" z-button z-drawer-close>Close</button>
          </z-drawer-footer>
        </z-drawer>
      </z-tab>

      <z-tab label="Service">
        <button type="button" z-button zType="secondary" (click)="openDrawer()">Open Drawer</button>
      </z-tab>
    </z-tab-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDrawerSwipeHandleComponent {
  private readonly drawerService = inject(ZardDrawerService);

  readonly visible = signal(false);

  openDrawer() {
    this.drawerService.create({
      zTitle: 'Drawer',
      zDescription: 'Drawer with a swipe handle.',
      zHandle: true,
      zContent: BOX,
      zOkText: null,
      zCancelText: 'Close',
    });
  }
}

nested

Open drawers from inside another drawer. Parent drawers stay mounted and stack behind the frontmost drawer.
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button';
import { ZardDrawerRef } from '@/shared/components/drawer/drawer-ref';
import { ZardDrawerImports } from '@/shared/components/drawer/drawer.imports';
import { injectDrawerData, ZardDrawerService } from '@/shared/components/drawer/drawer.service';
import type { ZardDrawerPlacement } from '@/shared/components/drawer/drawer.variants';
import { ZardTabsImports } from '@/shared/components/tabs/tabs.imports';

import { injectIsMobile } from './support/is-mobile';

interface NestedDrawerData {
  level: number;
  placement: ZardDrawerPlacement;
  handle: boolean;
}

const LEVELS = [
  { title: 'Drawer', description: 'Open another drawer from the same direction.' },
  { title: 'Nested Drawer', description: 'The parent drawer stays mounted behind this one.' },
  { title: 'Third Drawer', description: 'Two drawers are stacked behind this one.' },
];

/** Content the service renders; the last level closes itself, the others open the next one. */
@Component({
  selector: 'z-demo-drawer-nested-content',
  imports: [ZardButtonComponent],
  template: `
    <div class="bg-muted size-full min-h-32 rounded-2xl"></div>
    <div class="flex flex-col gap-2">
      @if (data.level < levels.length) {
        <button type="button" z-button zType="outline" (click)="openNext()">Open Nested Drawer</button>
      } @else {
        <button type="button" z-button (click)="drawerRef.close()">Close</button>
      }
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDrawerNestedContentComponent {
  private readonly drawerService = inject(ZardDrawerService);
  protected readonly drawerRef = inject(ZardDrawerRef);
  protected readonly data = injectDrawerData<NestedDrawerData>();
  protected readonly levels = LEVELS;

  openNext() {
    openNestedDrawer(this.drawerService, { ...this.data, level: this.data.level + 1 });
  }
}

function openNestedDrawer(drawerService: ZardDrawerService, data: NestedDrawerData) {
  const { title, description } = LEVELS[data.level - 1];

  drawerService.create({
    zTitle: title,
    zDescription: description,
    zContent: ZardDemoDrawerNestedContentComponent,
    zData: data,
    zPlacement: data.placement,
    zHandle: data.handle,
    zHideFooter: true,
  });
}

@Component({
  selector: 'z-demo-drawer-nested',
  imports: [ZardButtonComponent, ZardDrawerImports, ZardTabsImports],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <button type="button" z-button zType="secondary" (click)="first.set(true)">Open Drawer</button>

        <z-drawer [(zVisible)]="first" [zPlacement]="placement()" [zHandle]="isMobile()">
          <z-drawer-header>
            <z-drawer-title>Drawer</z-drawer-title>
            <z-drawer-description>Open another drawer from the same direction.</z-drawer-description>
          </z-drawer-header>
          <div class="flex-1 p-4">
            <div class="bg-muted size-full min-h-32 rounded-2xl"></div>
          </div>
          <z-drawer-footer>
            <button type="button" z-button zType="outline" (click)="second.set(true)">Open Nested Drawer</button>
          </z-drawer-footer>
        </z-drawer>

        <z-drawer [(zVisible)]="second" [zPlacement]="placement()" [zHandle]="isMobile()">
          <z-drawer-header>
            <z-drawer-title>Nested Drawer</z-drawer-title>
            <z-drawer-description>The parent drawer stays mounted behind this one.</z-drawer-description>
          </z-drawer-header>
          <div class="flex-1 p-4">
            <div class="bg-muted size-full min-h-32 rounded-2xl"></div>
          </div>
          <z-drawer-footer>
            <button type="button" z-button zType="outline" (click)="third.set(true)">Open Third Drawer</button>
          </z-drawer-footer>
        </z-drawer>

        <z-drawer [(zVisible)]="third" [zPlacement]="placement()" [zHandle]="isMobile()">
          <z-drawer-header>
            <z-drawer-title>Third Drawer</z-drawer-title>
            <z-drawer-description>Two drawers are stacked behind this one.</z-drawer-description>
          </z-drawer-header>
          <div class="flex-1 p-4">
            <div class="bg-muted size-full min-h-32 rounded-2xl"></div>
          </div>
          <z-drawer-footer>
            <button type="button" z-button z-drawer-close>Close</button>
          </z-drawer-footer>
        </z-drawer>
      </z-tab>

      <z-tab label="Service">
        <button type="button" z-button zType="secondary" (click)="openDrawer()">Open Drawer</button>
      </z-tab>
    </z-tab-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDrawerNestedComponent {
  private readonly drawerService = inject(ZardDrawerService);
  private readonly isMobileViewport = injectIsMobile();

  readonly first = signal(false);
  readonly second = signal(false);
  readonly third = signal(false);

  readonly isMobile = computed(() => this.isMobileViewport());
  readonly placement = computed<ZardDrawerPlacement>(() => (this.isMobile() ? 'bottom' : 'right'));

  openDrawer() {
    openNestedDrawer(this.drawerService, { level: 1, placement: this.placement(), handle: this.isMobile() });
  }
}

non modal

Set [zModal]="false" to allow interaction with the rest of the page while the drawer is open. A non-modal drawer keeps no mask, so an outside press does not dismiss it.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button';
import { ZardDrawerImports } from '@/shared/components/drawer/drawer.imports';
import { ZardDrawerService } from '@/shared/components/drawer/drawer.service';
import { ZardTabsImports } from '@/shared/components/tabs/tabs.imports';

/** Placeholder block the service renders as string content. */
const BOX = '<div class="bg-muted min-h-40 w-full rounded-2xl"></div>';

@Component({
  selector: 'z-demo-drawer-non-modal',
  imports: [ZardButtonComponent, ZardDrawerImports, ZardTabsImports],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <button type="button" z-button zType="outline" (click)="visible.set(true)">Non Modal</button>

        <z-drawer [(zVisible)]="visible" zPlacement="right" [zModal]="false">
          <z-drawer-header>
            <z-drawer-title>Non Modal Drawer</z-drawer-title>
            <z-drawer-description>The page behind stays scrollable and clickable.</z-drawer-description>
          </z-drawer-header>
          <div class="flex-1 p-4">
            <div class="bg-muted size-full rounded-2xl"></div>
          </div>
          <z-drawer-footer>
            <button type="button" z-button z-drawer-close>Close</button>
          </z-drawer-footer>
        </z-drawer>
      </z-tab>

      <z-tab label="Service">
        <button type="button" z-button zType="outline" (click)="openDrawer()">Non Modal</button>
      </z-tab>
    </z-tab-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDrawerNonModalComponent {
  private readonly drawerService = inject(ZardDrawerService);

  readonly visible = signal(false);

  openDrawer() {
    this.drawerService.create({
      zTitle: 'Non Modal Drawer',
      zDescription: 'The page behind stays scrollable and clickable.',
      zPlacement: 'right',
      zMask: false,
      zContent: BOX,
      zOkText: null,
      zCancelText: 'Close',
    });
  }
}

snap points

Use zSnapPoints to snap a drawer to preset heights. Numbers between 0 and 1 represent fractions of the viewport. Numbers greater than 1 are treated as pixel values. String values support px and rem units. Snap points apply to vertical drawers. Track the active one with [(zSnapPoint)]; at the largest snap point the drawer gets a data-expanded attribute you can style against.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button';
import { ZardDrawerImports } from '@/shared/components/drawer/drawer.imports';
import { ZardDrawerService } from '@/shared/components/drawer/drawer.service';
import type { ZardDrawerSnapPoint } from '@/shared/components/drawer/drawer.utils';
import { ZardTabsImports } from '@/shared/components/tabs/tabs.imports';

const SNAP_POINTS: ZardDrawerSnapPoint[] = ['31rem', 1];

/** Placeholder block the service renders as string content. */
const BOX = '<div class="bg-muted h-80 w-full rounded-2xl"></div>';

@Component({
  selector: 'z-demo-drawer-snap-points',
  imports: [ZardButtonComponent, ZardDrawerImports, ZardTabsImports],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <button type="button" z-button zType="outline" (click)="visible.set(true)">Open Snap Drawer</button>

        <z-drawer [(zVisible)]="visible" [zSnapPoints]="snapPoints" [(zSnapPoint)]="snapPoint" zHandle>
          <z-drawer-header>
            <z-drawer-title>Snap points</z-drawer-title>
            <z-drawer-description>
              Drag the drawer to snap between a compact peek and a near full-height view.
            </z-drawer-description>
          </z-drawer-header>
          <div class="flex-1 touch-pan-y overflow-y-auto p-4">
            <div class="bg-muted h-80 w-full rounded-2xl"></div>
          </div>
          <z-drawer-footer>
            <button type="button" z-button z-drawer-close>Close</button>
          </z-drawer-footer>
        </z-drawer>
      </z-tab>

      <z-tab label="Service">
        <button type="button" z-button zType="outline" (click)="openDrawer()">Open Snap Drawer</button>
      </z-tab>
    </z-tab-group>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDrawerSnapPointsComponent {
  private readonly drawerService = inject(ZardDrawerService);

  readonly snapPoints = SNAP_POINTS;
  readonly visible = signal(false);
  readonly snapPoint = signal<ZardDrawerSnapPoint | undefined>('31rem');

  openDrawer() {
    this.drawerService.create({
      zTitle: 'Snap points',
      zDescription: 'Drag the drawer to snap between a compact peek and a near full-height view.',
      zSnapPoints: SNAP_POINTS,
      zSnapPoint: '31rem',
      zHandle: true,
      zContent: BOX,
      zOkText: null,
      zCancelText: 'Close',
    });
  }
}

responsive

You can combine the Dialog and Drawer components to create a responsive dialog. This renders a Dialog on desktop and a Drawer on mobile.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

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

import { injectIsMobile } from './support/is-mobile';

@Component({
  selector: 'z-demo-drawer-profile-form',
  imports: [ZardButtonComponent, ZardInputComponent],
  template: `
    <form class="grid items-start gap-6" (submit)="$event.preventDefault()">
      <div class="grid gap-3">
        <label for="drawer-demo-email" class="text-sm leading-none font-medium select-none">Email</label>
        <input z-input id="drawer-demo-email" type="email" value="shadcn@example.com" />
      </div>
      <div class="grid gap-3">
        <label for="drawer-demo-username" class="text-sm leading-none font-medium select-none">Username</label>
        <input z-input id="drawer-demo-username" value="@shadcn" />
      </div>
      <button type="submit" z-button>Save changes</button>
    </form>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoDrawerProfileFormComponent {}

/** Same content, two surfaces: a dialog where there is room, a drawer where there is not. */
@Component({
  selector: 'z-demo-drawer-responsive',
  imports: [
    ZardButtonComponent,
    ZardDialogImports,
    ZardDrawerImports,
    ZardTabsImports,
    ZardDemoDrawerProfileFormComponent,
  ],
  template: `
    <z-tab-group>
      <z-tab label="Template">
        <button type="button" z-button zType="outline" (click)="visible.set(true)">Edit Profile</button>

        @if (isMobile()) {
          <z-drawer [(zVisible)]="visible">
            <z-drawer-header>
              <z-drawer-title>Edit profile</z-drawer-title>
              <z-drawer-description>
                Make changes to your profile here. Click save when you're done.
              </z-drawer-description>
            </z-drawer-header>
            <div class="p-4">
              <z-demo-drawer-profile-form />
            </div>
          </z-drawer>
        } @else {
          <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>
            <z-demo-drawer-profile-form />
          </z-dialog>
        }
      </z-tab>

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

  readonly visible = signal(false);
  readonly isMobile = this.isMobileViewport;

  open() {
    if (this.isMobile()) {
      this.drawerService.create({
        zTitle: 'Edit profile',
        zDescription: `Make changes to your profile here. Click save when you're done.`,
        zContent: ZardDemoDrawerProfileFormComponent,
        zHideFooter: true,
      });
      return;
    }

    this.dialogService.create({
      zTitle: 'Edit profile',
      zDescription: `Make changes to your profile here. Click save when you're done.`,
      zContent: ZardDemoDrawerProfileFormComponent,
      zHideFooter: true,
    });
  }
}

API Reference

z-drawerComponent

Root of a declarative drawer. Holds the open state and hosts the projected content. The panel exposes `data-placement`, `data-axis`, `data-state`, `data-swiping`, `data-snap-points`, `data-expanded` and `data-nested-open` (present while a drawer stacked on top of this one is open). Two CSS variables tune the panel: `--z-drawer-inset` (gap kept between the panel and the viewport edge, default `0.5rem`) and `--z-drawer-bleed` (fills that gap while the panel is dragged past its edge — set it for an edge-to-edge look). A drawer with `zSnapPoints` also publishes `--z-drawer-visible-size`, the pixels of the panel currently on screen, so content can size itself against whatever is reachable at the active snap point.

PropertyDescriptionTypeDefault
[zVisible] Open state, two-way bound boolean false
[zPlacement] Edge of the screen the drawer slides from. Also sets the swipe axis: top/bottom drag vertically, left/right drag horizontally 'top' | 'right' | 'bottom' | 'left' 'bottom'
[zSnapPoints] Sizes the drawer rests at. 0–1 is a fraction of the viewport, above that pixels, strings keep their CSS unit (number | string)[] -
[zSnapPoint] Active snap point, two-way bound through [(zSnapPoint)]. Defaults to the first one number | string -
[zDismissible] Whether swiping, the mask and Escape can close the drawer boolean true
[zHandle] Renders the swipe handle boolean false
[zModal] Renders the mask and blocks the page behind. Set false for a non-modal drawer boolean true
[class] Custom CSS classes applied to the panel ClassValue -
(zAfterOpen) Emitted once the drawer is attached EventEmitter<void> -
(zAfterClose) Emitted once the exit animation has finished EventEmitter<void> -

z-drawer-headerComponent

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

PropertyDescriptionTypeDefault
[class] Custom CSS classes to apply ClassValue -

z-drawer-footerComponent

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

PropertyDescriptionTypeDefault
[class] Custom CSS classes to apply ClassValue -

z-drawer-titleComponent

Accessible name of the drawer. 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-drawer-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-drawer-close]Component

Closes the drawer it is projected into. Works for declarative and service-opened drawers alike.

ZardDrawerOptionsComponent

Configuration accepted by `ZardDrawerService.create()`.

PropertyDescriptionTypeDefault
[zTitle] Drawer title text or template string | TemplateRef<void> -
[zDescription] Drawer description text or template string | TemplateRef<void> -
[zContent] Custom content component, template, or HTML string | TemplateRef<T> | Type<T> -
[zPlacement] Edge of the screen the drawer slides from 'top' | 'right' | 'bottom' | 'left' 'bottom'
[zSnapPoints] Sizes the drawer rests at (number | string)[] -
[zSnapPoint] Snap point the drawer opens at number | string -
[zDismissible] Whether swiping, the mask and Escape can close the drawer boolean true
[zHandle] Renders the swipe handle boolean false
[zMask] Renders the backdrop and blocks the page behind. Set false for a non-modal drawer boolean true
[zMaskClosable] Whether clicking outside closes the drawer. Also requires zDismissible boolean true
[zClosable] Whether to show the close button boolean true
[zDuration] Exit animation duration in ms number 450
[zOkText] OK button text, null to hide button string | null 'OK'
[zCancelText] Cancel button text, null to hide button string | null 'Cancel'
[zOkIcon] OK button icon — registered icon name or inline SVG string string -
[zCancelIcon] Cancel button icon — registered icon name or inline SVG string string -
[zOkDestructive] Whether OK button should have destructive styling boolean false
[zOkDisabled] Whether OK button should be disabled boolean false
[zHideFooter] Whether to hide the footer with action buttons boolean false
[zCustomClasses] Additional CSS classes to apply ClassValue -
[zOnOk] OK button click handler EventEmitter<T> | OnClickCallback<T> -
[zOnCancel] Cancel button click handler EventEmitter<T> | OnClickCallback<T> -
[zData] Data to pass to custom content components object -
[zViewContainerRef] View container for rendering custom content ViewContainerRef -

ZardDrawerRefComponent

Reference returned by `ZardDrawerService.create()`, used to observe and close the drawer.

PropertyDescriptionTypeDefault
[close] Closes the drawer, optionally with a result (result?: R) => void -
[isClosing] Signal that turns true once the drawer starts closing Signal<boolean> false
[result] Signal holding the result passed to close() Signal<R | undefined> undefined
[componentInstance] Signal with the instance of the component rendered as content Signal<T | null> null
github iconwhatsapp icondiscord iconX icon

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