Modal Dialog
Build a flexible modal dialog component with backdrop, animations, and focus management
Learn to build a production-ready modal dialog with proper focus management, backdrop click handling, animations, and accessibility. Perfect for confirmations, forms, and detailed content displays.
What We're Building
A modal dialog that supports:
Open/close with smooth animations
Backdrop click to close (optional)
Escape key to close
Focus trap (keyboard focus stays within modal)
Return focus to trigger when closed
Accessible with ARIA attributes
Portal rendering (renders outside parent context)
Scrollable content
Component Code
modal-dialog.ts
import { bindable, IEventAggregator } from 'aurelia';
import { resolve } from '@aurelia/kernel';
import { queueTask } from '@aurelia/runtime';
import { IPlatform } from '@aurelia/runtime-html';
export class ModalDialog {
@bindable open = false;
@bindable closeOnBackdropClick = true;
@bindable closeOnEscape = true;
@bindable size: 'small' | 'medium' | 'large' | 'full' = 'medium';
private platform = resolve(IPlatform);
private element?: HTMLElement;
private modalElement?: HTMLElement;
private previousActiveElement?: HTMLElement;
private focusableElements: HTMLElement[] = [];
openChanged(newValue: boolean) {
if (newValue) {
this.onOpen();
} else {
this.onClose();
}
}
attaching(initiator: HTMLElement) {
this.element = initiator;
this.modalElement = this.element.querySelector('[data-modal]') as HTMLElement;
}
detaching() {
// Clean up if modal is still open
if (this.open) {
this.cleanupModal();
}
}
closeModal() {
this.open = false;
}
handleBackdropClick(event: MouseEvent) {
// Only close if clicking the backdrop itself, not content inside
if (this.closeOnBackdropClick && event.target === event.currentTarget) {
this.closeModal();
}
}
handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape' && this.closeOnEscape) {
event.preventDefault();
this.closeModal();
return;
}
// Tab key focus trap
if (event.key === 'Tab') {
this.handleTabKey(event);
}
}
private onOpen() {
// Store currently focused element to return focus later
this.previousActiveElement = document.activeElement as HTMLElement;
// Prevent body scroll
document.body.style.overflow = 'hidden';
// Wait for DOM to render, then focus first element
queueTask(() => {
this.updateFocusableElements();
this.focusFirstElement();
});
}
private onClose() {
this.cleanupModal();
}
private cleanupModal() {
// Restore body scroll
document.body.style.overflow = '';
// Return focus to element that opened the modal
if (this.previousActiveElement) {
this.previousActiveElement.focus();
this.previousActiveElement = undefined;
}
}
private updateFocusableElements() {
if (!this.modalElement) return;
const focusableSelectors = [
'a[href]',
'button:not([disabled])',
'textarea:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
this.focusableElements = Array.from(
this.modalElement.querySelectorAll(focusableSelectors)
) as HTMLElement[];
}
private focusFirstElement() {
const firstFocusable = this.focusableElements[0];
if (firstFocusable) {
firstFocusable.focus();
}
}
private handleTabKey(event: KeyboardEvent) {
if (this.focusableElements.length === 0) return;
const firstElement = this.focusableElements[0];
const lastElement = this.focusableElements[this.focusableElements.length - 1];
const activeElement = document.activeElement as HTMLElement;
if (event.shiftKey) {
// Shift + Tab: Move backwards
if (activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
}
} else {
// Tab: Move forwards
if (activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
}
}modal-dialog.html
modal-dialog.css
Usage Examples
Basic Modal
Confirmation Dialog
Form Modal
Full-Screen Modal
Testing
Accessibility Features
This modal follows WCAG 2.1 guidelines:
✅ Focus Trap: Tab key cycles through focusable elements within modal
✅ Focus Management: Focuses first element when opened, returns focus when closed
✅ Keyboard Support: Escape key closes modal
✅ ARIA Attributes:
role="dialog",aria-modal="true"for screen readers✅ Body Scroll Lock: Prevents scrolling background content
Enhancements
1. Add Transition Animations
Use Aurelia's animation system for smoother transitions:
2. Add Confirmation Before Close
3. Add Modal Service
Create a global modal service for programmatic modals:
Best Practices
Focus Management: Always return focus to the trigger element
Body Scroll: Lock body scroll to prevent confusion
Escape Key: Always allow Escape to close (unless critical action)
Backdrop Click: Make it configurable, disable for forms with unsaved changes
Portal Rendering: For complex apps, render modals in a portal at document root
Stacking: Support multiple modals with z-index management
Summary
You've built a fully-featured modal dialog with:
✅ Smooth animations
✅ Focus trap and management
✅ Keyboard support
✅ Accessible markup
✅ Multiple sizes
✅ Customizable behavior
This modal is production-ready and handles all common use cases!
Last updated
Was this helpful?