AvesraAvesrabeta

Drawer

Slide-out panel for supplementary content and actions

Import

import {
  AvDrawerImports,
  AvDrawerService,
} from '@avesra/angular';

Usage

import { Component } from '@angular/core';
import {
  AvButtonComponent,
  AvDrawerImports,
} from '@avesra/angular';

@Component({
  selector: 'app-drawer-basic-demo',
  imports: [
    AvDrawerImports,
    AvButtonComponent,
  ],
  template: `<av-drawer placement="right">
      <button av-button variant="secondary" av-drawer-trigger>Open Drawer</button>
      <ng-template avDrawerContent>
        <div av-drawer-dialog>
          <div av-drawer-header>
            <h2 av-drawer-heading>Drawer Title</h2>
          </div>
          <div av-drawer-body>
            <p>
              This is a right-side drawer. It slides in from the edge of the screen with a smooth
              CSS transition.
            </p>
          </div>
          <div av-drawer-footer>
            <button av-button variant="secondary" av-drawer-close>Cancel</button>
            <button av-button av-drawer-close>Confirm</button>
          </div>
        </div>
      </ng-template>
    </av-drawer>`,
})
export class DrawerBasicDemo {}

Anatomy

Declare the trigger and an ng-template avDrawerContent inside av-drawer. The overlay content is rendered into a CDK overlay only while the drawer is open. Optional pieces include av-drawer-close-trigger and av-drawer-handle.

<av-drawer placement="right" backdrop="opaque">
  <button av-button av-drawer-trigger>Open Drawer</button>
  <ng-template avDrawerContent>
    <div av-drawer-dialog>
      <av-drawer-handle /> <!-- Optional: Drag handle -->
      <av-drawer-close-trigger /> <!-- Optional: Close button -->
      <div av-drawer-header>
        <h2 av-drawer-heading></h2>
      </div>
      <div av-drawer-body></div>
      <div av-drawer-footer></div>
    </div>
  </ng-template>
</av-drawer>

Placement

Set placement on av-drawer — top, bottom, left, or right.

import { Component } from '@angular/core';
import { TitleCasePipe } from '@angular/common';
import {
  AvButtonComponent,
  AvDrawerImports,
  type AvDrawerPlacement,
} from '@avesra/angular';

@Component({
  selector: 'app-drawer-placements-demo',
  imports: [
    AvDrawerImports,
    AvButtonComponent,
    TitleCasePipe,
  ],
  template: `<div class="flex flex-wrap gap-4">
      @for (placement of placements; track placement) {
        <av-drawer [placement]="placement">
          <button av-button variant="secondary" av-drawer-trigger>
            {{ placement | titlecase }}
          </button>
          <ng-template avDrawerContent>
            <div av-drawer-dialog>
              <av-drawer-close-trigger />
              @if (placement === 'bottom') {
                <av-drawer-handle />
              }
              <div av-drawer-header>
                <h2 av-drawer-heading>{{ placement | titlecase }} Drawer</h2>
              </div>
              <div av-drawer-body>
                <p>
                  This drawer slides in from the <strong>{{ placement }}</strong> edge of the
                  screen.
                </p>
              </div>
              <div av-drawer-footer>
                <button av-button variant="secondary" av-drawer-close>Cancel</button>
                <button av-button av-drawer-close>Done</button>
              </div>
              @if (placement === 'top') {
                <av-drawer-handle />
              }
            </div>
          </ng-template>
        </av-drawer>
      }
    </div>`,
})
export class DrawerPlacementsDemo {
  readonly placements: readonly AvDrawerPlacement[] = ['bottom', 'top', 'left', 'right'];
}

Non-Dismissable

Set [dismissable]="false" on av-drawer to prevent closing by clicking the backdrop. The user must interact with the drawer's action buttons.

import { Component } from '@angular/core';
import {
  AvButtonComponent,
  AvDrawerImports,
} from '@avesra/angular';

@Component({
  selector: 'app-drawer-non-dismissable-demo',
  imports: [
    AvDrawerImports,
    AvButtonComponent,
  ],
  template: `<av-drawer [dismissable]="false">
      <button av-button variant="secondary" av-drawer-trigger>Important Action</button>
      <ng-template avDrawerContent>
        <div av-drawer-dialog>
          <div av-drawer-header>
            <h2 av-drawer-heading>Confirm Action</h2>
          </div>
          <div av-drawer-body>
            <p>
              This drawer cannot be dismissed by clicking outside. You must use one of the buttons
              below.
            </p>
          </div>
          <div av-drawer-footer>
            <button av-button variant="secondary" av-drawer-close>Cancel</button>
            <button av-button av-drawer-close>Confirm</button>
          </div>
        </div>
      </ng-template>
    </av-drawer>`,
})
export class DrawerNonDismissableDemo {}

Scrollable Content

The av-drawer-body automatically handles overflow with native scrolling.

import { Component } from '@angular/core';
import {
  AvButtonComponent,
  AvDrawerImports,
} from '@avesra/angular';

@Component({
  selector: 'app-drawer-scrollable-content-demo',
  imports: [
    AvDrawerImports,
    AvButtonComponent,
  ],
  template: `<av-drawer>
      <button av-button variant="secondary" av-drawer-trigger>Terms &amp; Conditions</button>
      <ng-template avDrawerContent>
        <div av-drawer-dialog>
          <av-drawer-handle />
          <av-drawer-close-trigger />
          <div av-drawer-header>
            <h2 av-drawer-heading>Terms &amp; Conditions</h2>
          </div>
          <div av-drawer-body>
            @for (paragraph of scrollParagraphs; track paragraph) {
              <p class="mb-3">
                Paragraph {{ paragraph }}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
                Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit
                risus, sed porttitor quam.
              </p>
            }
          </div>
          <div av-drawer-footer>
            <button av-button variant="secondary" av-drawer-close>Decline</button>
            <button av-button av-drawer-close>Accept</button>
          </div>
        </div>
      </ng-template>
    </av-drawer>`,
})
export class DrawerScrollableContentDemo {
  readonly scrollParagraphs = Array.from({ length: 20 }, (_, index) => index + 1);
}

Controlled State

With signal()

Control the drawer using Angular's signal and [(open)] for simple state management.

Status: closed

With open() / toggle()

Wrap a signal with helper methods for a cleaner API — open(), close(), and toggle().

Status: closed

import { Component, signal } from '@angular/core';
import {
  AvButtonComponent,
  AvDrawerImports,
} from '@avesra/angular';

@Component({
  selector: 'app-drawer-controlled-demo',
  imports: [
    AvDrawerImports,
    AvButtonComponent,
  ],
  template: `<div class="flex max-w-md flex-col gap-8">
      <div class="flex flex-col gap-3">
        <h3 class="text-lg font-semibold text-foreground">With signal()</h3>
        <p class="text-sm leading-relaxed text-pretty text-muted">
          Control the drawer using Angular's <code class="text-foreground">signal</code> and
          <code class="text-foreground">[(open)]</code> for simple state management.
        </p>
        <div class="flex flex-col items-start gap-3 rounded-2xl bg-surface p-4 shadow-sm">
          <div class="flex w-full items-center justify-between">
            <p class="text-xs text-muted">
              Status:
              <span class="font-mono font-medium text-foreground">
                {{ isOpen() ? 'open' : 'closed' }}
              </span>
            </p>
          </div>
          <div class="flex gap-2">
            <button av-button size="sm" variant="secondary" type="button" (click)="openSignal()">
              Open Drawer
            </button>
            <button av-button size="sm" variant="tertiary" type="button" (click)="toggleSignal()">
              Toggle
            </button>
          </div>
        </div>

        <av-drawer [(open)]="isOpen" placement="right">
          <ng-template avDrawerContent>
            <div av-drawer-dialog>
              <av-drawer-close-trigger />
              <div av-drawer-header>
                <h2 av-drawer-heading>Controlled with signal()</h2>
              </div>
              <div av-drawer-body>
                <p>
                  This drawer is controlled by Angular's <code>signal</code>. Bind
                  <code>[(open)]</code> to manage the drawer state externally.
                </p>
              </div>
              <div av-drawer-footer>
                <button av-button variant="secondary" av-drawer-close>Close</button>
              </div>
            </div>
          </ng-template>
        </av-drawer>
      </div>

      <div class="flex flex-col gap-3">
        <h3 class="text-lg font-semibold text-foreground">With open() / toggle()</h3>
        <p class="text-sm leading-relaxed text-pretty text-muted">
          Wrap a signal with helper methods for a cleaner API —
          <code class="text-foreground">open()</code>, <code class="text-foreground">close()</code>,
          and <code class="text-foreground">toggle()</code>.
        </p>
        <div class="flex flex-col items-start gap-3 rounded-2xl bg-surface p-4 shadow-sm">
          <div class="flex w-full items-center justify-between">
            <p class="text-xs text-muted">
              Status:
              <span class="font-mono font-medium text-foreground">
                {{ overlayOpen() ? 'open' : 'closed' }}
              </span>
            </p>
          </div>
          <div class="flex gap-2">
            <button av-button size="sm" variant="secondary" type="button" (click)="openOverlay()">
              Open Drawer
            </button>
            <button av-button size="sm" variant="tertiary" type="button" (click)="toggleOverlay()">
              Toggle
            </button>
          </div>
        </div>

        <av-drawer [(open)]="overlayOpen" placement="right">
          <ng-template avDrawerContent>
            <div av-drawer-dialog>
              <av-drawer-close-trigger />
              <div av-drawer-header>
                <h2 av-drawer-heading>Controlled with open() / toggle()</h2>
              </div>
              <div av-drawer-body>
                <p>
                  Dedicated methods keep callers simple — use <code>open()</code>,
                  <code>close()</code>, or <code>toggle()</code> instead of updating the signal
                  inline.
                </p>
              </div>
              <div av-drawer-footer>
                <button av-button variant="secondary" av-drawer-close>Close</button>
              </div>
            </div>
          </ng-template>
        </av-drawer>
      </div>
    </div>`,
})
export class DrawerControlledDemo {
  readonly isOpen = signal(false);
  readonly overlayOpen = signal(false);

  openSignal(): void {
    this.isOpen.set(true);
  }

  toggleSignal(): void {
    this.isOpen.update((value) => !value);
  }

  openOverlay(): void {
    this.overlayOpen.set(true);
  }

  toggleOverlay(): void {
    this.overlayOpen.update((value) => !value);
  }
}

With Form

import { Component } from '@angular/core';
import {
  AvButtonComponent,
  AvDrawerImports,
  AvInputComponent,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-drawer-with-form-demo',
  imports: [
    AvDrawerImports,
    AvButtonComponent,
    AvInputComponent,
    AvLabelComponent,
  ],
  template: `<av-drawer placement="right">
      <button av-button variant="secondary" av-drawer-trigger>Edit Profile</button>
      <ng-template avDrawerContent>
        <div av-drawer-dialog>
          <av-drawer-close-trigger />
          <div av-drawer-header>
            <h2 av-drawer-heading>Edit Profile</h2>
          </div>
          <div av-drawer-body>
            <form class="flex flex-col gap-4">
              <div class="flex w-full flex-col gap-1">
                <label av-label for="drawer-name">Name</label>
                <input
                  av-input
                  full-width
                  id="drawer-name"
                  name="name"
                  type="text"
                  placeholder="Enter your name"
                  variant="secondary"
                />
              </div>
              <div class="flex w-full flex-col gap-1">
                <label av-label for="drawer-email">Email</label>
                <input
                  av-input
                  full-width
                  id="drawer-email"
                  name="email"
                  type="email"
                  placeholder="Enter your email"
                  variant="secondary"
                />
              </div>
              <div class="flex w-full flex-col gap-1">
                <label av-label for="drawer-bio">Bio</label>
                <input
                  av-input
                  full-width
                  id="drawer-bio"
                  name="bio"
                  type="text"
                  placeholder="Tell us about yourself"
                  variant="secondary"
                />
              </div>
            </form>
          </div>
          <div av-drawer-footer>
            <button av-button variant="secondary" av-drawer-close>Cancel</button>
            <button av-button av-drawer-close>Save Changes</button>
          </div>
        </div>
      </ng-template>
    </av-drawer>`,
})
export class DrawerWithFormDemo {}

Backdrop Variants

Choose a backdrop style with backdrop on av-drawer: opaque (default), blur, or transparent.

import { Component } from '@angular/core';
import { TitleCasePipe } from '@angular/common';
import {
  AvButtonComponent,
  AvDrawerImports,
  type AvDrawerBackdropVariant,
} from '@avesra/angular';

@Component({
  selector: 'app-drawer-backdrop-variants-demo',
  imports: [
    AvDrawerImports,
    AvButtonComponent,
    TitleCasePipe,
  ],
  template: `<div class="flex flex-wrap gap-4">
      @for (variant of backdropVariants; track variant) {
        <av-drawer [backdrop]="variant">
          <button av-button variant="secondary" av-drawer-trigger>
            {{ variant | titlecase }}
          </button>
          <ng-template avDrawerContent>
            <div av-drawer-dialog>
              <av-drawer-handle />
              <av-drawer-close-trigger />
              <div av-drawer-header>
                <h2 av-drawer-heading>Backdrop: {{ variant | titlecase }}</h2>
              </div>
              <div av-drawer-body>
                <p>
                  This drawer uses the <code>{{ variant }}</code> backdrop variant.
                </p>
              </div>
              <div av-drawer-footer>
                <button av-button class="w-full" av-drawer-close>Close</button>
              </div>
            </div>
          </ng-template>
        </av-drawer>
      }
    </div>`,
})
export class DrawerBackdropVariantsDemo {
  readonly backdropVariants: readonly AvDrawerBackdropVariant[] = ['opaque', 'blur', 'transparent'];
}

Custom Backdrop

Combine the backdrop variant with utility classes passed through backdrop-class for gradients and other overlays.

import { Component } from '@angular/core';
import {
  AvButtonComponent,
  AvDrawerImports,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-drawer-custom-backdrop-demo',
  imports: [
    AppIconComponent,
    AvDrawerImports,
    AvButtonComponent,
  ],
  template: `<av-drawer
  placement="right"
  backdrop="blur"
  backdrop-class="bg-linear-to-l from-black/80 via-black/40 to-transparent dark:from-zinc-800/80 dark:via-zinc-800/40"
>
  <button av-button variant="secondary" av-drawer-trigger>Custom Backdrop</button>
  <ng-template avDrawerContent>
    <div av-drawer-dialog>
      <av-drawer-close-trigger />
      <div av-drawer-header>
        <h2 av-drawer-heading class="flex items-center gap-2">
          <app-icon class="text-accent" icon="solar:stars-linear" size="20" />
          Premium Backdrop
        </h2>
      </div>
      <div av-drawer-body>
        <p>
          This backdrop features a horizontal gradient that transitions from a dark color at the
          right edge to transparency, combined with the blur variant. Utility classes passed through
          <code>backdrop-class</code> layer over the variant styles.
        </p>
      </div>
      <div av-drawer-footer class="flex-col-reverse sm:flex-row">
        <button av-button class="w-full" variant="secondary" av-drawer-close>Close</button>
        <button av-button class="w-full" av-drawer-close>Amazing!</button>
      </div>
    </div>
  </ng-template>
</av-drawer>`,
})
export class DrawerCustomBackdropDemo {}

Service

Use AvDrawerService.open() to mount any component as a drawer without declaring overlay markup. Pass data through the config, read it with AV_DRAWER_DATA, and receive the close result from afterClosed().

Open any component as a drawer with AvDrawerService.open() — no overlay markup in your template.

import { Component, inject, signal } from '@angular/core';
import {
  AV_DRAWER_DATA,
  AvButtonComponent,
  AvDrawerImports,
  AvDrawerService,
} from '@avesra/angular';

interface WorkspacePanelData {
  workspace: string;
  members: number;
}

@Component({
  selector: 'app-drawer-service-panel',
  imports: [
    AvDrawerImports,
    AvButtonComponent,
  ],
  template: `<div av-drawer-dialog>
      <av-drawer-close-trigger />
      <div av-drawer-header>
        <h2 av-drawer-heading>{{ data.workspace }}</h2>
      </div>
      <div av-drawer-body>
        <p>
          This panel was opened with <code>AvDrawerService.open()</code>. It receives
          <code>{{ data.members }}</code> members through the <code>data</code> config and injects
          it with <code>AV_DRAWER_DATA</code>.
        </p>
      </div>
      <div av-drawer-footer>
        <button av-button variant="secondary" [av-drawer-close]="'cancelled'">Cancel</button>
        <button av-button [av-drawer-close]="'invited'">Invite Members</button>
      </div>
    </div>`,
})
export class DrawerServicePanel {
  readonly data = inject(AV_DRAWER_DATA) as WorkspacePanelData;
}

@Component({
  selector: 'app-drawer-service-demo',
  imports: [AvButtonComponent],
  template: `<div class="flex max-w-md flex-col gap-4">
      <p class="text-sm text-muted">
        Open any component as a drawer with <code>AvDrawerService.open()</code> — no overlay markup
        in your template.
      </p>
      <button av-button variant="secondary" (click)="openWorkspacePanel()">Open Workspace Panel</button>
      @if (lastResult() !== null) {
        <p class="text-sm text-muted">
          Last result:
          <code class="font-mono text-foreground">{{ lastResult() }}</code>
        </p>
      }
    </div>`,
})
export class DrawerServiceDemo {
  private readonly drawer = inject(AvDrawerService);

  readonly lastResult = signal<string | null>(null);

  openWorkspacePanel(): void {
    this.drawer
      .open<DrawerServicePanel, WorkspacePanelData, string>(DrawerServicePanel, {
        data: { workspace: 'Avesra Design Team', members: 12 },
        placement: 'right',
      })
      .afterClosed()
      .subscribe((result) => {
        this.lastResult.set(result ?? 'dismissed');
      });
  }
}

Styling

Passing Tailwind CSS classes

Style the overlay chrome with backdrop-class and content-class on av-drawer, and pass utility classes directly on div[av-drawer-dialog] and the other part hosts.

import { Component } from '@angular/core';
import {
  AvButtonComponent,
  AvDrawerImports,
} from '@avesra/angular';

@Component({
  selector: 'app-drawer-custom-styling-demo',
  imports: [
    AvDrawerImports,
    AvButtonComponent,
  ],
  template: `<av-drawer placement="right" backdrop="blur">
      <button av-button variant="secondary" av-drawer-trigger>Open filters</button>
      <ng-template avDrawerContent>
        <div av-drawer-dialog class="border-l border-border/80 bg-surface">
          <div av-drawer-header>
            <h2 av-drawer-heading class="text-foreground">Filters</h2>
          </div>
          <div av-drawer-body>
            <p class="text-sm text-muted">Narrow results by status, owner, or date.</p>
          </div>
          <div av-drawer-footer>
            <button av-button variant="secondary" av-drawer-close>Cancel</button>
            <button av-button av-drawer-close>Apply</button>
          </div>
        </div>
      </ng-template>
    </av-drawer>`,
})
export class DrawerCustomStylingDemo {}

Customizing the component classes

To customize the Drawer classes, use the @layer components directive. Learn more.

@layer components {
  .av-drawer__backdrop {
    @apply bg-gradient-to-br from-black/50 to-black/70;
  }

  .av-drawer__dialog {
    @apply rounded-2xl border border-white/10 shadow-2xl;
  }

  .av-drawer__header {
    @apply text-center;
  }

  .av-drawer__close-trigger {
    @apply rounded-full bg-white/10 hover:bg-white/20;
  }
}

Avesra follows the BEM methodology so component variants and states stay reusable and easy to customize. The overlay is portaled into a CDK pane with class .av-drawer-overlay-pane.

CSS Classes

The Drawer component uses these CSS classes:

Base Classes

  • .av-drawer-overlay-pane — Full-viewport CDK overlay container
  • .av-drawer__trigger — Trigger element that opens the drawer
  • .av-drawer__backdrop — Overlay backdrop behind the drawer
  • .av-drawer__content — Positioning wrapper for the drawer panel
  • .av-drawer__dialog — The drawer panel itself
  • .av-drawer__header — Header section for titles
  • .av-drawer__heading — Main title text
  • .av-drawer__body — Main scrollable content area
  • .av-drawer__footer — Footer section for actions
  • .av-drawer__handle — Visual drag handle indicator
  • .av-drawer__close-trigger — Close button element

Backdrop Variants

  • .av-drawer__backdrop--opaque — Opaque colored backdrop (default)
  • .av-drawer__backdrop--blur — Blurred backdrop with glass effect
  • .av-drawer__backdrop--transparent — Transparent backdrop (no overlay)

Placement Variants

  • .av-drawer__content--bottom — Slides up from the bottom edge (default)
  • .av-drawer__content--top — Slides down from the top edge
  • .av-drawer__content--left — Slides in from the left edge
  • .av-drawer__content--right — Slides in from the right edge
  • .av-drawer__dialog--top — Top placement padding adjustment

Interactive States

The component supports these interactive states:

  • Focus::focus-visible or [data-focus-visible="true"] — Applied to trigger and close button
  • Hover::hover or [data-hovered="true"] — Applied to close button on hover
  • Active::active or [data-pressed="true"] — Applied to trigger and close button when pressed
  • Entering:[data-entering="true"] — During opening animation
  • Exiting:[data-exiting="true"] — During closing animation
  • Placement:[data-placement="*"] — Drawer edge (top, bottom, left, right)

API Reference

Overlay configuration lives on av-drawer; the dialog and its child parts are composed inside ng-template avDrawerContent. Close actions use av-drawer-close on interactive elements.

PropTypeDefaultDescription
openbooleanfalseControls whether the drawer is open. Supports two-way binding with [(open)] (av-drawer).
dismissablebooleantrueWhether clicking the backdrop closes the drawer (av-drawer).
keyboard-dismiss-disabledbooleanfalseDisables closing via the Escape key (av-drawer).
backdrop'opaque' | 'blur' | 'transparent''opaque'Backdrop visual variant (av-drawer).
backdrop-classstring''Extra CSS classes merged onto the visual backdrop after the variant BEM classes (av-drawer).
content-classstring''Extra CSS classes merged onto the content positioning wrapper after the BEM classes (av-drawer).
placement'top' | 'bottom' | 'left' | 'right''bottom'Drawer slide direction (av-drawer).
placement'top' | 'bottom' | 'left' | 'right' | undefined—Slide direction. Inherits from av-drawer when omitted (div[av-drawer-dialog]).
aria-labelstring—Accessible label when the trigger has no visible text ([av-drawer-trigger]).

Accessibility

Implements the WAI-ARIA Dialog pattern:

  • Focus trap: Focus locked within the drawer when open
  • Keyboard:Escape closes (when enabled), Tab cycles elements
  • Screen readers: Proper ARIA attributes on the dialog
  • Scroll lock: Body scroll disabled when open

Made with ❤ by SyntaxHertz. Open source and available on GitHub.