Modal
Dialog overlay for focused user interactions and important content.
Import
import {
AvModalImports,
AvModalService,
} from '@avesra/angular';Usage
import { Component } from '@angular/core';
import {
AvButtonComponent,
AvModalImports,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-modal-basic-demo',
imports: [
AppIconComponent,
AvModalImports,
AvButtonComponent,
],
template: `<av-modal>
<button av-button variant="secondary" av-modal-trigger>Open Modal</button>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<av-modal-close-trigger />
<div av-modal-header>
<div av-modal-icon class="bg-default text-foreground">
<app-icon icon="solar:rocket-linear" size="20" />
</div>
<h2 av-modal-heading>Welcome to Avesra</h2>
</div>
<div av-modal-body>
<p>
A beautiful, fast, and modern Angular UI library for building accessible and
customizable web applications with ease.
</p>
</div>
<div av-modal-footer>
<button av-button class="w-full" av-modal-close>Continue</button>
</div>
</div>
</ng-template>
</av-modal>`,
})
export class ModalBasicDemo {}Anatomy
Declare the trigger and an ng-template avModalContent inside av-modal. The overlay content is rendered into a CDK overlay only while the modal is open. Optional pieces include av-modal-close-trigger and av-modal-icon.
<av-modal>
<button av-button av-modal-trigger>Open Modal</button>
<ng-template avModalContent>
<div av-modal-dialog>
<!-- Optional: Close button -->
<av-modal-close-trigger />
<div av-modal-header>
<!-- Optional: Icon -->
<div av-modal-icon></div>
<h2 av-modal-heading></h2>
</div>
<div av-modal-body></div>
<div av-modal-footer></div>
</div>
</ng-template>
</av-modal>Placement
Set placement on av-modal — auto, top, center, or bottom.
import { Component } from '@angular/core';
import { TitleCasePipe } from '@angular/common';
import {
AvButtonComponent,
AvModalImports,
type AvModalPlacement,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-modal-placements-demo',
imports: [
TitleCasePipe,
AppIconComponent,
AvModalImports,
AvButtonComponent,
],
template: `<div class="flex flex-wrap gap-4">
@for (placement of placements; track placement) {
<av-modal [placement]="placement">
<button av-button variant="secondary" av-modal-trigger>
{{ placement | titlecase }}
</button>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<av-modal-close-trigger />
<div av-modal-header>
<div av-modal-icon class="bg-default text-foreground">
<app-icon icon="solar:rocket-linear" size="20" />
</div>
<h2 av-modal-heading>Placement: {{ placement | titlecase }}</h2>
</div>
<div av-modal-body>
<p>
This modal uses the <code>{{ placement }}</code> placement option. Try different
placements to see how the modal positions itself on the screen.
</p>
</div>
<div av-modal-footer>
<button av-button class="w-full" av-modal-close>Continue</button>
</div>
</div>
</ng-template>
</av-modal>
}
</div>`,
})
export class ModalPlacementsDemo {
readonly placements: readonly AvModalPlacement[] = ['auto', 'top', 'center', 'bottom'];
}Backdrop Variants
Choose a backdrop style with backdrop on av-modal: opaque (default), blur, or transparent.
import { Component } from '@angular/core';
import { TitleCasePipe } from '@angular/common';
import {
AvButtonComponent,
AvModalImports,
type AvModalBackdropVariant,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-modal-backdrop-variants-demo',
imports: [
TitleCasePipe,
AppIconComponent,
AvModalImports,
AvButtonComponent,
],
template: `<div class="flex flex-wrap gap-4">
@for (variant of backdropVariants; track variant) {
<av-modal [backdrop]="variant">
<button av-button variant="secondary" av-modal-trigger>
{{ variant | titlecase }}
</button>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<av-modal-close-trigger />
<div av-modal-header>
<div av-modal-icon class="bg-default text-foreground">
<app-icon icon="solar:rocket-linear" size="20" />
</div>
<h2 av-modal-heading>Backdrop: {{ variant | titlecase }}</h2>
</div>
<div av-modal-body>
<p>
This modal uses the <code>{{ variant }}</code> backdrop variant. Compare the
different visual effects: opaque provides full opacity, blur adds a backdrop
filter, and transparent removes the background.
</p>
</div>
<div av-modal-footer>
<button av-button class="w-full" av-modal-close>Continue</button>
</div>
</div>
</ng-template>
</av-modal>
}
</div>`,
})
export class ModalBackdropVariantsDemo {
readonly backdropVariants: readonly AvModalBackdropVariant[] = ['opaque', 'blur', 'transparent'];
}Sizes
Pass size to av-modal — xs, sm, md, lg, cover, or full. Individual dialogs can override it with size on div[av-modal-dialog].
import { Component } from '@angular/core';
import { TitleCasePipe } from '@angular/common';
import {
AvButtonComponent,
AvModalImports,
type AvModalSize,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-modal-sizes-demo',
imports: [
TitleCasePipe,
AppIconComponent,
AvModalImports,
AvButtonComponent,
],
template: `<div class="flex flex-wrap gap-4">
@for (size of sizes; track size) {
<av-modal [size]="size">
<button av-button variant="secondary" av-modal-trigger>
{{ size | titlecase }}
</button>
<ng-template avModalContent>
<div av-modal-dialog>
<av-modal-close-trigger />
<div av-modal-header>
<div av-modal-icon class="bg-default text-foreground">
<app-icon icon="solar:rocket-linear" size="20" />
</div>
<h2 av-modal-heading>Size: {{ size | titlecase }}</h2>
</div>
<div av-modal-body>
<p>{{ sizeDescription(size) }}</p>
</div>
<div av-modal-footer>
<button av-button variant="secondary" av-modal-close>Cancel</button>
<button av-button av-modal-close>Confirm</button>
</div>
</div>
</ng-template>
</av-modal>
}
</div>`,
})
export class ModalSizesDemo {
readonly sizes: readonly AvModalSize[] = ['xs', 'sm', 'md', 'lg', 'cover', 'full'];
sizeDescription(size: AvModalSize): string {
switch (size) {
case 'cover':
return 'This modal uses the cover size variant. It spans the full screen with margins, keeping rounded corners and standard padding.';
case 'full':
return 'This modal uses the full size variant. It occupies the entire viewport without margins, rounded corners, or shadows.';
default:
return `This modal uses the ${size} size variant. On mobile, sizes adapt to near full-width; on desktop each size has a different max-width.`;
}
}
}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,
AvModalImports,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-modal-custom-backdrop-demo',
imports: [
AppIconComponent,
AvModalImports,
AvButtonComponent,
],
template: `<av-modal
backdrop="blur"
backdrop-class="bg-linear-to-t from-black/80 via-black/40 to-transparent dark:from-zinc-800/80 dark:via-zinc-800/40"
>
<button av-button variant="secondary" av-modal-trigger>Custom Backdrop</button>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<av-modal-close-trigger />
<div av-modal-header class="items-center text-center">
<div av-modal-icon class="bg-accent-soft text-accent-soft-foreground">
<app-icon icon="solar:stars-linear" size="20" />
</div>
<h2 av-modal-heading>Premium Backdrop</h2>
</div>
<div av-modal-body>
<p>
This backdrop features a gradient that transitions from a dark color at the bottom to
transparency at the top, combined with the blur variant. Utility classes passed through
<code>backdrop-class</code> layer over the variant styles.
</p>
</div>
<div av-modal-footer class="flex-col-reverse">
<button av-button class="w-full" av-modal-close>Amazing!</button>
<button av-button class="w-full" variant="secondary" av-modal-close>Close</button>
</div>
</div>
</ng-template>
</av-modal>`,
})
export class ModalCustomBackdropDemo {}Dismiss Behavior
Set [dismissable]="false" to prevent backdrop dismissal, and keyboard-dismiss-disabled to disable Escape. Both live on av-modal.
dismissable
Controls whether the modal can be dismissed by clicking the overlay backdrop. Defaults to true. Set to false to require an explicit close action.
keyboard-dismiss-disabled
When set, the Escape key will not close the modal. Users must use an explicit close action or the backdrop (when dismissable).
import { Component } from '@angular/core';
import {
AvButtonComponent,
AvModalImports,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-modal-dismiss-behavior-demo',
imports: [
AppIconComponent,
AvModalImports,
AvButtonComponent,
],
template: `<div class="flex max-w-sm flex-col gap-6">
<div class="flex flex-col gap-2">
<h3 class="text-lg font-semibold">dismissable</h3>
<p class="text-sm text-muted">
Controls whether the modal can be dismissed by clicking the overlay backdrop. Defaults to
<code>true</code>. Set to <code>false</code> to require an explicit close action.
</p>
<av-modal [dismissable]="false">
<button av-button variant="secondary" av-modal-trigger>Open Modal</button>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<av-modal-close-trigger />
<div av-modal-header>
<div av-modal-icon class="bg-default text-foreground">
<app-icon icon="solar:info-circle-linear" size="20" />
</div>
<h2 av-modal-heading>dismissable = false</h2>
<p class="text-sm leading-5 text-muted">Clicking the backdrop won't close this modal</p>
</div>
<div av-modal-body>
<p>
Try clicking outside this modal on the overlay — it won't close. Use the close
button or press Escape to dismiss it.
</p>
</div>
<div av-modal-footer>
<button av-button class="w-full" av-modal-close>Close</button>
</div>
</div>
</ng-template>
</av-modal>
</div>
<div class="flex flex-col gap-2">
<h3 class="text-lg font-semibold">keyboard-dismiss-disabled</h3>
<p class="text-sm text-muted">
When set, the Escape key will not close the modal. Users must use an explicit close action or
the backdrop (when dismissable).
</p>
<av-modal keyboard-dismiss-disabled>
<button av-button variant="secondary" av-modal-trigger>Open Modal</button>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<av-modal-close-trigger />
<div av-modal-header>
<div av-modal-icon class="bg-default text-foreground">
<app-icon icon="solar:info-circle-linear" size="20" />
</div>
<h2 av-modal-heading>keyboard-dismiss-disabled</h2>
<p class="text-sm leading-5 text-muted">Escape key is disabled</p>
</div>
<div av-modal-body>
<p>
Press Escape — nothing happens. Use the close button or click the overlay backdrop
to dismiss this modal.
</p>
</div>
<div av-modal-footer>
<button av-button class="w-full" av-modal-close>Close</button>
</div>
</div>
</ng-template>
</av-modal>
</div>
</div>`,
})
export class ModalDismissBehaviorDemo {}Close Methods
Close with av-modal-close on buttons, or set [(open)] to false when you need custom logic first.
Using av-modal-close
The simplest way to close a modal. Add av-modal-close to any interactive element inside the overlay. When clicked, it closes the modal.
Using [(open)]
Drive close from your component by setting open to false. Useful when you need validation or other logic before dismissing.
import { Component, signal } from '@angular/core';
import {
AvButtonComponent,
AvModalImports,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-modal-close-methods-demo',
imports: [
AppIconComponent,
AvModalImports,
AvButtonComponent,
],
template: `<div class="flex max-w-2xl flex-col gap-8">
<div class="flex flex-col gap-2">
<h3 class="text-lg font-semibold">Using av-modal-close</h3>
<p class="text-sm text-muted">
The simplest way to close a modal. Add <code>av-modal-close</code> to any interactive element
inside the overlay. When clicked, it closes the modal.
</p>
<av-modal>
<button av-button variant="secondary" av-modal-trigger>Open Modal</button>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<div av-modal-header>
<div av-modal-icon class="bg-accent-soft text-accent-soft-foreground">
<app-icon icon="solar:info-circle-linear" size="20" />
</div>
<h2 av-modal-heading>Using av-modal-close</h2>
</div>
<div av-modal-body>
<p>
Click either button below — both have <code>av-modal-close</code> and will close
the modal automatically.
</p>
</div>
<div av-modal-footer>
<button av-button variant="secondary" av-modal-close>Cancel</button>
<button av-button av-modal-close>Confirm</button>
</div>
</div>
</ng-template>
</av-modal>
</div>
<div class="flex flex-col gap-2">
<h3 class="text-lg font-semibold">Using [(open)]</h3>
<p class="text-sm text-muted">
Drive close from your component by setting <code>open</code> to <code>false</code>. Useful
when you need validation or other logic before dismissing.
</p>
<av-modal [(open)]="programmaticOpen">
<button av-button variant="secondary" av-modal-trigger>Open Modal</button>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<div av-modal-header>
<div av-modal-icon class="bg-success-soft text-success-soft-foreground">
<app-icon icon="solar:check-circle-linear" size="20" />
</div>
<h2 av-modal-heading>Using [(open)]</h2>
</div>
<div av-modal-body>
<p>
The buttons below call <code>closeProgrammatic()</code>, which sets
<code>open</code> to <code>false</code> after optional custom logic.
</p>
</div>
<div av-modal-footer>
<button av-button variant="secondary" (click)="closeProgrammatic()">Cancel</button>
<button av-button (click)="closeProgrammatic()">Confirm</button>
</div>
</div>
</ng-template>
</av-modal>
</div>
</div>`,
})
export class ModalCloseMethodsDemo {
readonly programmaticOpen = signal(false);
closeProgrammatic(): void {
this.programmaticOpen.set(false);
}
}Programmatic
Use AvModalService.open() to mount any component as a modal without declaring overlay markup. Pass data through the config, read it with AV_MODAL_DATA, and receive the close result from afterClosed().
Open any component as a modal with AvModalService.open() — no overlay markup in your template.
import { Component, inject, signal } from '@angular/core';
import {
AV_MODAL_DATA,
AvButtonComponent,
AvModalImports,
AvModalService,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
interface WorkspacePanelData {
workspace: string;
members: number;
}
@Component({
selector: 'app-modal-service-panel',
imports: [
AppIconComponent,
AvModalImports,
AvButtonComponent,
],
template: `<div av-modal-dialog>
<av-modal-close-trigger />
<div av-modal-header>
<div av-modal-icon class="bg-accent-soft text-accent-soft-foreground">
<app-icon icon="solar:users-group-rounded-linear" size="20" />
</div>
<h2 av-modal-heading>{{ data.workspace }}</h2>
</div>
<div av-modal-body>
<p>
This panel was opened with <code>AvModalService.open()</code>. It receives
<code>{{ data.members }}</code> members through the <code>data</code> config and injects
it with <code>AV_MODAL_DATA</code>.
</p>
</div>
<div av-modal-footer>
<button av-button variant="secondary" [av-modal-close]="'cancelled'">Cancel</button>
<button av-button [av-modal-close]="'invited'">Invite Members</button>
</div>
</div>`,
})
export class ModalServicePanel {
readonly data = inject(AV_MODAL_DATA) as WorkspacePanelData;
}
@Component({
selector: 'app-modal-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 modal with <code>AvModalService.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 ModalServiceDemo {
private readonly modal = inject(AvModalService);
readonly lastResult = signal<string | null>(null);
openWorkspacePanel(): void {
this.modal
.open<ModalServicePanel, WorkspacePanelData, string>(ModalServicePanel, {
data: { workspace: 'Avesra Design Team', members: 12 },
size: 'sm',
})
.afterClosed()
.subscribe((result) => {
this.lastResult.set(result ?? 'dismissed');
});
}
}Custom Animations
Override enter/exit motion with Tailwind animation utilities passed through backdrop-class and container-class using data-[entering] / data-[exiting] selectors.
import { Component } from '@angular/core';
import {
AvButtonComponent,
AvModalImports,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
const kinematicBackdrop = [
'data-[entering]:duration-400',
'data-[entering]:ease-[cubic-bezier(0.16,1,0.3,1)]',
'data-[exiting]:duration-200',
'data-[exiting]:ease-[cubic-bezier(0.7,0,0.84,0)]',
].join(' ');
const kinematicContainer = [
'data-[entering]:animate-in',
'data-[entering]:fade-in-0',
'data-[entering]:zoom-in-95',
'data-[entering]:duration-400',
'data-[entering]:ease-[cubic-bezier(0.16,1,0.3,1)]',
'data-[exiting]:animate-out',
'data-[exiting]:fade-out-0',
'data-[exiting]:zoom-out-95',
'data-[exiting]:duration-200',
'data-[exiting]:ease-[cubic-bezier(0.7,0,0.84,0)]',
].join(' ');
const fluidBackdrop = [
'data-[entering]:duration-500',
'data-[entering]:ease-[cubic-bezier(0.25,1,0.5,1)]',
'data-[exiting]:duration-200',
'data-[exiting]:ease-[cubic-bezier(0.5,0,0.75,0)]',
].join(' ');
const fluidContainer = [
'data-[entering]:animate-in',
'data-[entering]:fade-in-0',
'data-[entering]:slide-in-from-bottom-4',
'data-[entering]:duration-500',
'data-[entering]:ease-[cubic-bezier(0.25,1,0.5,1)]',
'data-[exiting]:animate-out',
'data-[exiting]:fade-out-0',
'data-[exiting]:slide-out-to-bottom-2',
'data-[exiting]:duration-200',
'data-[exiting]:ease-[cubic-bezier(0.5,0,0.75,0)]',
].join(' ');
@Component({
selector: 'app-modal-custom-animations-demo',
imports: [
AppIconComponent,
AvModalImports,
AvButtonComponent,
],
template: `<div class="flex flex-wrap gap-4">
<av-modal [backdrop-class]="kinematicBackdrop" [container-class]="kinematicContainer">
<button av-button variant="secondary" av-modal-trigger>Kinematic Scale</button>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<av-modal-close-trigger />
<div av-modal-header>
<div av-modal-icon class="bg-default text-foreground">
<app-icon icon="solar:star-fall-linear" size="20" />
</div>
<h2 av-modal-heading>Kinematic Scale Animation</h2>
</div>
<div av-modal-body>
<p class="mt-1">
Physics-based elastic scaling. Simulates a high-damping spring system with fast
transient response and prolonged settling time. Ideal for Modals and Popovers.
</p>
</div>
<div av-modal-footer>
<button av-button variant="tertiary" av-modal-close>Close</button>
<button av-button av-modal-close>Try Again</button>
</div>
</div>
</ng-template>
</av-modal>
<av-modal [backdrop-class]="fluidBackdrop" [container-class]="fluidContainer">
<button av-button variant="secondary" av-modal-trigger>Fluid Slide</button>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<av-modal-close-trigger />
<div av-modal-header>
<div av-modal-icon class="bg-default text-foreground">
<app-icon icon="solar:upload-linear" size="20" />
</div>
<h2 av-modal-heading>Fluid Slide Animation</h2>
</div>
<div av-modal-body>
<p class="mt-1">
Simulates movement through a medium with fluid resistance. Eliminates mechanical
linearity for a natural, grounded feel. Perfect for Bottom Sheets or Toasts.
</p>
</div>
<div av-modal-footer>
<button av-button variant="tertiary" av-modal-close>Close</button>
<button av-button av-modal-close>Try Again</button>
</div>
</div>
</ng-template>
</av-modal>
</div>`,
})
export class ModalCustomAnimationsDemo {
readonly kinematicBackdrop = kinematicBackdrop;
readonly kinematicContainer = kinematicContainer;
readonly fluidBackdrop = fluidBackdrop;
readonly fluidContainer = fluidContainer;
}Scroll Behavior
Use scroll="inside" (default) on av-modal to scroll only the body, or scroll="outside" to scroll the entire dialog within the viewport.
import { Component, signal } from '@angular/core';
import { TitleCasePipe } from '@angular/common';
import {
AvButtonComponent,
AvModalImports,
type AvModalScroll,
} from '@avesra/angular';
@Component({
selector: 'app-modal-scroll-behavior-demo',
imports: [
AvModalImports,
AvButtonComponent,
TitleCasePipe,
],
template: `<div class="flex flex-col gap-4">
<div class="flex flex-wrap gap-2">
<button
av-button
size="sm"
[variant]="scrollMode() === 'inside' ? 'primary' : 'secondary'"
(click)="setScrollMode('inside')"
>
Inside
</button>
<button
av-button
size="sm"
[variant]="scrollMode() === 'outside' ? 'primary' : 'secondary'"
(click)="setScrollMode('outside')"
>
Outside
</button>
</div>
<av-modal [scroll]="scrollMode()">
<button av-button variant="secondary" av-modal-trigger>
Open Modal ({{ scrollMode() | titlecase }})
</button>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<av-modal-close-trigger />
<div av-modal-header>
<h2 av-modal-heading>Scroll: {{ scrollMode() | titlecase }}</h2>
<p class="text-sm leading-5 text-muted">
Compare scroll behaviors — inside keeps content scrollable within the modal, outside
allows scrolling the entire dialog in the viewport.
</p>
</div>
<div av-modal-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-modal-footer>
<button av-button variant="secondary" av-modal-close>Cancel</button>
<button av-button av-modal-close>Confirm</button>
</div>
</div>
</ng-template>
</av-modal>
</div>`,
})
export class ModalScrollBehaviorDemo {
readonly scrollMode = signal<AvModalScroll>('inside');
readonly scrollParagraphs = Array.from({ length: 30 }, (_, index) => index + 1);
setScrollMode(mode: AvModalScroll): void {
this.scrollMode.set(mode);
}
}Controlled State
Bind [(open)] on av-modal to drive visibility from outside the overlay tree — open, close, and toggle programmatically.
Control the modal with a signal and [(open)] on av-modal.
Status: closed
import { Component, signal } from '@angular/core';
import {
AvButtonComponent,
AvModalImports,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-modal-controlled-demo',
imports: [
AppIconComponent,
AvModalImports,
AvButtonComponent,
],
template: `<div class="flex max-w-md flex-col gap-3">
<p class="text-sm text-muted">
Control the modal with a signal and <code>[(open)]</code> on <code>av-modal</code>.
</p>
<div class="flex flex-col items-start gap-3 rounded-2xl bg-surface p-4 shadow-sm">
<p class="text-xs text-muted">
Status:
<span class="font-mono font-medium text-foreground">
{{ open() ? 'open' : 'closed' }}
</span>
</p>
<div class="flex gap-2">
<button av-button size="sm" variant="secondary" (click)="openModal()">Open Modal</button>
<button av-button size="sm" variant="tertiary" (click)="toggle()">Toggle</button>
</div>
</div>
<av-modal [(open)]="open">
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<av-modal-close-trigger />
<div av-modal-header>
<div av-modal-icon class="bg-accent-soft text-accent-soft-foreground">
<app-icon icon="solar:check-circle-linear" size="20" />
</div>
<h2 av-modal-heading>Controlled with [(open)]</h2>
</div>
<div av-modal-body>
<p>
This modal is controlled by a signal. Bind <code>[(open)]</code> to manage visibility
from outside the overlay tree.
</p>
</div>
<div av-modal-footer>
<button av-button variant="secondary" av-modal-close>Cancel</button>
<button av-button av-modal-close>Confirm</button>
</div>
</div>
</ng-template>
</av-modal>
</div>`,
})
export class ModalControlledDemo {
readonly open = signal(false);
openModal(): void {
this.open.set(true);
}
toggle(): void {
this.open.update((value) => !value);
}
}With Form
Place form fields inside av-modal-body and wire submit actions in av-modal-footer. Focus remains trapped while the modal is open.
import { Component } from '@angular/core';
import {
AvButtonComponent,
AvLabelComponent,
AvModalImports,
AvSurfaceComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-modal-with-form-demo',
imports: [
AppIconComponent,
AvModalImports,
AvButtonComponent,
AvLabelComponent,
AvSurfaceComponent,
],
template: `<av-modal placement="auto">
<button av-button variant="secondary" av-modal-trigger>Open Contact Form</button>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-md">
<av-modal-close-trigger />
<div av-modal-header>
<div av-modal-icon class="bg-accent-soft text-accent-soft-foreground">
<app-icon icon="solar:letter-linear" size="20" />
</div>
<h2 av-modal-heading>Contact Us</h2>
<p class="mt-1.5 text-sm leading-5 text-muted">
Fill out the form below and we'll get back to you. The modal adapts when the keyboard
appears on mobile.
</p>
</div>
<div av-modal-body class="p-6">
<div av-surface variant="default" class="rounded-2xl p-4">
<form class="flex flex-col gap-4" (submit)="$event.preventDefault()">
<div class="flex w-full flex-col gap-1.5">
<label av-label for="modal-contact-name">Name</label>
<input
id="modal-contact-name"
class="h-10 w-full rounded-xl border border-border bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-focus"
placeholder="Enter your name"
/>
</div>
<div class="flex w-full flex-col gap-1.5">
<label av-label for="modal-contact-email">Email</label>
<input
id="modal-contact-email"
type="email"
class="h-10 w-full rounded-xl border border-border bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-focus"
placeholder="Enter your email"
/>
</div>
<div class="flex w-full flex-col gap-1.5">
<label av-label for="modal-contact-phone">Phone</label>
<input
id="modal-contact-phone"
type="tel"
class="h-10 w-full rounded-xl border border-border bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-focus"
placeholder="Enter your phone number"
/>
</div>
<div class="flex w-full flex-col gap-1.5">
<label av-label for="modal-contact-company">Company</label>
<input
id="modal-contact-company"
class="h-10 w-full rounded-xl border border-border bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-focus"
placeholder="Enter your company name"
/>
</div>
<div class="flex w-full flex-col gap-1.5">
<label av-label for="modal-contact-message">Message</label>
<input
id="modal-contact-message"
class="h-10 w-full rounded-xl border border-border bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-focus"
placeholder="Enter your message"
/>
</div>
</form>
</div>
</div>
<div av-modal-footer>
<button av-button variant="secondary" av-modal-close>Cancel</button>
<button av-button av-modal-close>Send Message</button>
</div>
</div>
</ng-template>
</av-modal>`,
})
export class ModalWithFormDemo {}Custom Trigger
Apply av-modal-trigger to any focusable element — cards, links, or icon buttons — instead of a plain button.
Settings
Manage your preferences
import { Component } from '@angular/core';
import {
AvButtonComponent,
AvModalImports,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';
@Component({
selector: 'app-modal-custom-trigger-demo',
imports: [
AppIconComponent,
AvModalImports,
AvButtonComponent,
],
template: `<av-modal>
<div
av-modal-trigger
class="group flex max-w-xs cursor-pointer items-center gap-3 rounded-2xl bg-surface p-4 shadow-xs select-none hover:bg-surface-secondary"
>
<div
class="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent-soft text-accent-soft-foreground"
>
<app-icon icon="solar:settings-linear" size="24" />
</div>
<div class="flex flex-1 flex-col gap-0.5">
<p class="text-sm font-semibold">Settings</p>
<p class="text-xs text-muted">Manage your preferences</p>
</div>
</div>
<ng-template avModalContent>
<div av-modal-dialog class="sm:max-w-[360px]">
<av-modal-close-trigger />
<div av-modal-header>
<div av-modal-icon class="bg-accent-soft text-accent-soft-foreground">
<app-icon icon="solar:settings-linear" size="20" />
</div>
<h2 av-modal-heading>Settings</h2>
</div>
<div av-modal-body>
<p>
Use <code>av-modal-trigger</code> on any focusable element to create custom triggers
beyond standard buttons. This example shows a card-style trigger with an icon and
descriptive text.
</p>
</div>
<div av-modal-footer>
<button av-button variant="secondary" av-modal-close>Cancel</button>
<button av-button av-modal-close>Save</button>
</div>
</div>
</ng-template>
</av-modal>`,
})
export class ModalCustomTriggerDemo {}Styling
Passing Tailwind CSS classes
Style the overlay chrome with backdrop-class and container-class on av-modal, and pass utility classes directly on div[av-modal-dialog] and the other part hosts.
import { Component } from '@angular/core';
import {
AvButtonComponent,
AvModalImports,
} from '@avesra/angular';
@Component({
selector: 'app-modal-custom-styling-demo',
imports: [
AvModalImports,
AvButtonComponent,
],
template: `<av-modal backdrop-class="bg-black/80" container-class="items-start pt-20">
<button av-button variant="secondary" av-modal-trigger>Open Modal</button>
<ng-template avModalContent>
<div
av-modal-dialog
class="bg-linear-to-br from-accent to-accent-soft text-accent-foreground sm:max-w-[360px]"
>
<av-modal-close-trigger />
<div av-modal-header>
<h2 av-modal-heading>Custom Styled Modal</h2>
</div>
<div av-modal-body>
<p>
This modal has custom styling applied via <code>backdrop-class</code> and
<code>container-class</code> on the root, plus utility classes on the dialog host.
</p>
</div>
<div av-modal-footer>
<button av-button av-modal-close>Close</button>
</div>
</div>
</ng-template>
</av-modal>`,
})
export class ModalCustomStylingDemo {}Customizing the component classes
To customize the Modal classes, use the @layer components directive. Learn more.
@layer components {
.av-modal__backdrop {
@apply bg-gradient-to-br from-black/50 to-black/70;
}
.av-modal__dialog {
@apply rounded-2xl border border-white/10 shadow-2xl;
}
.av-modal__header {
@apply text-center;
}
.av-modal__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.
CSS Classes
The Modal component uses these CSS classes:
Base Classes
.av-modal__trigger— Trigger element that opens the modal.av-modal__backdrop— Overlay backdrop behind the modal.av-modal__container— Positioning wrapper with placement support.av-modal__dialog— Modal content container.av-modal__header— Header section for titles and icons.av-modal__heading— Dialog title.av-modal__icon— Optional header icon.av-modal__body— Main content area.av-modal__footer— Footer section for actions.av-modal__close-trigger— Close button element
Backdrop Variants
.av-modal__backdrop--opaque— Opaque colored backdrop (default).av-modal__backdrop--blur— Blurred backdrop with glass effect.av-modal__backdrop--transparent— Transparent backdrop (no overlay)
Scroll Variants
.av-modal__container--scroll-outside— Enables scrolling the entire modal.av-modal__dialog--scroll-inside— Constrains modal height for body scrolling.av-modal__body--scroll-inside— Makes only the body scrollable.av-modal__body--scroll-outside— Allows full-page scrolling
Interactive States
The component supports these interactive states:
- Focus:
:focus-visibleor[data-focus-visible="true"]— Applied to trigger, dialog, and close button - Hover:
:hoveror[data-hovered="true"]— Applied to close button on hover - Active:
:activeor[data-pressed="true"]— Applied to close button when pressed - Entering:
[data-entering]— During opening animation - Exiting:
[data-exiting]— During closing animation - Placement:
[data-placement="*"]— Modal position (auto,top,center,bottom)
API Reference
Overlay configuration lives on av-modal; the dialog and its child parts are composed inside ng-template avModalContent. Close actions use av-modal-close on interactive elements.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Controls whether the modal is open. Supports two-way binding with [(open)] (av-modal). |
dismissable | boolean | true | Whether clicking the backdrop closes the modal (av-modal). |
keyboard-dismiss-disabled | boolean | false | Disables closing via the Escape key (av-modal). |
backdrop | 'opaque' | 'blur' | 'transparent' | 'opaque' | Backdrop visual variant (av-modal). |
backdrop-class | string | '' | Extra CSS classes merged onto the visual backdrop after the variant BEM classes (av-modal). |
container-class | string | '' | Extra CSS classes merged onto the overlay container after the BEM classes — useful for custom enter/exit motion (av-modal). |
placement | 'auto' | 'top' | 'center' | 'bottom' | 'auto' | Modal position on screen (av-modal). |
scroll | 'inside' | 'outside' | 'inside' | Scroll behavior for long content (av-modal). |
size | 'xs' | 'sm' | 'md' | 'lg' | 'cover' | 'full' | 'md' | Maximum width / layout preset (av-modal). |
scroll | 'inside' | 'outside' | undefined | — | Scroll behavior. Inherits from av-modal when omitted (div[av-modal-dialog]). |
size | 'xs' | 'sm' | 'md' | 'lg' | 'cover' | 'full' | undefined | — | Size preset. Inherits from av-modal when omitted (div[av-modal-dialog]). |
aria-label | string | — | Accessible label when the trigger has no visible text ([av-modal-trigger]). |
Accessibility
Implements the WAI-ARIA Dialog pattern:
- Focus trap: Focus locked within the modal
- Keyboard:Escape closes (when enabled), Tab cycles elements
- Screen readers: Proper ARIA attributes on the dialog
- Scroll lock: Body scroll disabled when open