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 & 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 & 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-visibleor[data-focus-visible="true"]— Applied to trigger and close button - Hover:
:hoveror[data-hovered="true"]— Applied to close button on hover - Active:
:activeor[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.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Controls whether the drawer is open. Supports two-way binding with [(open)] (av-drawer). |
dismissable | boolean | true | Whether clicking the backdrop closes the drawer (av-drawer). |
keyboard-dismiss-disabled | boolean | false | Disables closing via the Escape key (av-drawer). |
backdrop | 'opaque' | 'blur' | 'transparent' | 'opaque' | Backdrop visual variant (av-drawer). |
backdrop-class | string | '' | Extra CSS classes merged onto the visual backdrop after the variant BEM classes (av-drawer). |
content-class | string | '' | 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-label | string | — | 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