Modal
Dialog windows that overlay main content, focusing attention on specific tasks or messages
Overview
Modals are dialog windows that overlay the main content, focusing user attention on a specific task or message. They're essential for confirmations, forms, alerts, and detailed views without leaving the current page context.
Key Features
- Semi-transparent backdrop that dims background content
- Keyboard navigation with Escape key to close
- Click outside (backdrop) to close option
- Smooth fade animations for opening and closing
- Multiple size variants (small, default, large)
- Event callbacks for lifecycle management
- Programmatic control (open, close, toggle)
When to Use Modals
- Confirmations: Delete actions, irreversible operations, important decisions
- Forms: Login, registration, quick edits, contact forms
- Detail Views: Product details, image galleries, extended information
- Alerts & Messages: Success notifications, error messages, warnings
- Multi-Step Processes: Wizards, guided tours, onboarding flows
Basic Usage
// HTML structure
<div id="my-modal" class="modal">
<div class="modal-header">
<h5 class="modal-title">Modal Title</h5>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
Modal content goes here.
</div>
<div class="modal-footer">
<button class="btn btn-secondary modal-close-btn">Cancel</button>
<button class="btn btn-primary">Save</button>
</div>
</div>
// JavaScript
const modal = Domma.elements.modal('#my-modal', {
backdrop: true,
backdropClose: true,
keyboard: true
});
// Control the modal
modal.open(); // Opens the modal
modal.close(); // Closes the modal
modal.toggle(); // Toggles open/close
⚠️ Important: Domma vs Bootstrap Structure Domma uses a FLAT structure - do NOT use Bootstrap's
.modal-dialogand.modal-contentwrapper divs! ❌ WRONG (Bootstrap structure):✅ CORRECT (Domma structure):......
Quick Start
Create a simple modal in under a minute. This example includes a trigger button and basic modal structure.
Simplest Modal
// HTML
<button class="btn btn-primary" id="open-simple-modal">Open Modal</button>
<div id="simple-modal" class="modal">
<div class="modal-header">
<h5 class="modal-title">Welcome!</h5>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
<p>This is a simple modal dialog.</p>
</div>
<div class="modal-footer">
<button class="btn btn-primary modal-close-btn">Got It</button>
</div>
</div>
// JavaScript
const modal = Domma.elements.modal('#simple-modal');
$('#open-simple-modal').on('click', () => {
modal.open();
});
Welcome!
This is a simple modal dialog. You can close it by clicking the X, the backdrop, or pressing Escape.
<button type="button" class="btn btn-primary" id="quick-start-btn">Try It - Open Modal</button>
<div id="quick-start-modal" class="modal">
<div class="modal-header">
<h5 class="modal-title">Welcome!</h5>
<button type="button" class="modal-close">×</button>
</div>
<div class="modal-body">
<p>This is a simple modal dialog. You can close it by clicking the X, the backdrop, or pressing
Escape.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary modal-close-btn">Got It</button>
</div>
</div>/**
* Simplest Modal - from public/showcase/elements/modal.
*/
export function mount(root) {
const quickStartModal = E.modal(root.querySelector('#quick-start-modal'));
$(root).find('#quick-start-btn').on('click', () => {
quickStartModal.open();
});
// Auto-wire all modal-close-btn buttons
$('.modal-close-btn').each(function () {
$(this).on('click', function () {
const modal = $(this).closest('.modal').get(0);
if (modal && modal.id) {
// Find and close the corresponding modal instance
const instances = [quickStartModal, tutorialModal, smallModal, largeModal,
noBackdropModal, successModal, scrollModal, parentModal, childModal];
instances.forEach(inst => {
if (inst && inst.el && inst.el.id === modal.id) {
inst.close();
}
});
}
});
});
}Step-by-Step Tutorial
Build a confirmation modal for deleting items, step by step.
Step 1: Create the HTML Structure
Start with the modal markup and trigger button:
<button class="btn btn-danger" id="delete-btn">Delete Item</button>
<div id="confirm-modal" class="modal">
<div class="modal-header">
<h5 class="modal-title">Confirm Deletion</h5>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this item?</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary modal-close-btn">Cancel</button>
<button class="btn btn-danger" id="confirm-delete">Delete</button>
</div>
</div>
Step 2: Initialise the Modal
Create modal instance with default options:
const confirmModal = Domma.elements.modal('#confirm-modal', {
backdrop: true, // Show backdrop
backdropClose: true, // Allow closing by clicking backdrop
keyboard: true // Allow closing with Escape key
});
Step 3: Add Open Trigger
Make the delete button open the modal:
$('#delete-btn').on('click', () => {
confirmModal.open();
});
Step 4: Handle Confirmation
Add logic when user confirms deletion:
$('#confirm-delete').on('click', () => {
// Perform deletion
console.log('Item deleted');
// Close the modal
confirmModal.close();
// Show success message (could use Toast component)
alert('Item deleted successfully');
});
Step 5: Add Event Callbacks (Optional)
Track modal lifecycle with callbacks:
const confirmModal = Domma.elements.modal('#confirm-modal', {
backdrop: true,
backdropClose: true,
keyboard: true,
onOpen: () => {
console.log('Modal opened');
// Could disable page actions
},
onClose: () => {
console.log('Modal closing');
},
onClosed: () => {
console.log('Modal fully closed');
}
});
Step 6: Complete Working Demo
Confirm Deletion
Are you sure you want to delete this item?
This action cannot be undone.
<button type="button" class="btn btn-danger" id="tutorial-delete-btn">Delete Item</button>
<div class="demo-result mt-2" id="tutorial-result"></div>
<div id="tutorial-modal" class="modal">
<div class="modal-header">
<h5 class="modal-title">Confirm Deletion</h5>
<button type="button" class="modal-close">×</button>
</div>
<div class="modal-body">
<p class="mb-2">Are you sure you want to delete this item?</p>
<p class="text-sm">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-close-btn">Cancel</button>
<button type="button" class="btn btn-danger" id="tutorial-confirm">Delete</button>
</div>
</div>/**
* Step 6: Complete Working Demo - from public/showcase/elements/modal.
*/
export function mount(root) {
// Tutorial
const tutorialModal = E.modal(root.querySelector('#tutorial-modal'), {
backdrop: true,
backdropClose: true,
keyboard: true,
onOpen: () => console.log('Tutorial modal opened'),
onClosed: () => console.log('Tutorial modal closed')
});
$(root).find('#tutorial-delete-btn').on('click', () => {
tutorialModal.open();
});
$(root).find('#tutorial-confirm').on('click', () => {
$(root).find('#tutorial-result').removeClass('hidden').html('<span class="text-success">✓ Item deleted successfully at ' + D().format('HH:mm:ss') + '</span>');
tutorialModal.close();
});
}Examples & Variations
Example 1: Small Modal
Compact modal for simple messages:
Quick Message
This is a small modal for brief messages.
<button type="button" class="btn btn-secondary" id="open-small">Open Small Modal</button>
<div id="small-modal" class="modal modal-sm">
<div class="modal-header">
<h5 class="modal-title">Quick Message</h5>
<button type="button" class="modal-close">×</button>
</div>
<div class="modal-body">
<p>This is a small modal for brief messages.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm modal-close-btn">OK</button>
</div>
</div>/**
* Example 1: Small Modal - from public/showcase/elements/modal.
*/
export function mount(root) {
// Examples
const smallModal = E.modal(root.querySelector('#small-modal'));
$(root).find('#open-small').on('click', () => smallModal.open());
}Example 2: Large Modal with Form
Larger modal for complex content:
Contact Form
<button type="button" class="btn btn-primary" id="open-large">Open Large Modal</button>
<div id="large-modal" class="modal modal-lg">
<div class="modal-header">
<h5 class="modal-title">Contact Form</h5>
<button type="button" class="modal-close">×</button>
</div>
<div class="modal-body">
<div class="grid grid-cols-2 gap-4">
<div class="form-group">
<label class="form-label">First Name</label>
<input type="text" class="form-input" placeholder="John">
</div>
<div class="form-group">
<label class="form-label">Last Name</label>
<input type="text" class="form-input" placeholder="Doe">
</div>
<div class="form-group col-span-full">
<label class="form-label">Email</label>
<input type="email" class="form-input" placeholder="john@example.com">
</div>
<div class="form-group col-span-full">
<label class="form-label">Message</label>
<textarea class="form-textarea" rows="3" placeholder="Your message..."></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-close-btn">Cancel</button>
<button type="button" class="btn btn-primary">Send Message</button>
</div>
</div>/**
* Example 2: Large Modal with Form - from public/showcase/elements/modal.
*/
export function mount(root) {
const largeModal = E.modal(root.querySelector('#large-modal'));
$(root).find('#open-large').on('click', () => largeModal.open());
}Example 3: No Backdrop Close
Modal that requires explicit action to close:
Important Notice
This modal requires you to click a button to close it.
You cannot close it by clicking outside or pressing Escape.
<button type="button" class="btn btn-warning" id="open-no-backdrop">Open Forced Modal</button>
<div id="no-backdrop-modal" class="modal">
<div class="modal-header">
<h5 class="modal-title">Important Notice</h5>
</div>
<div class="modal-body">
<p class="mb-2">This modal requires you to click a button to close it.</p>
<p class="text-sm">You cannot close it by clicking outside or pressing Escape.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary modal-close-btn">I Understand</button>
</div>
</div>/**
* Example 3: No Backdrop Close - from public/showcase/elements/modal.
*/
export function mount(root) {
const noBackdropModal = E.modal(root.querySelector('#no-backdrop-modal'), {
backdropClose: false,
keyboard: false
});
$(root).find('#open-no-backdrop').on('click', () => noBackdropModal.open());
}Example 4: Success Message
Modal for positive feedback:
✓ Success!
Your changes have been saved successfully.
<button type="button" class="btn btn-success" id="open-success">Show Success</button>
<div id="success-modal" class="modal modal-sm">
<div class="modal-header">
<h5 class="modal-title m-0">✓ Success!</h5>
<button type="button" class="modal-close">×</button>
</div>
<div class="modal-body text-center">
<div class="text-5xl text-success mb-3">✓
</div>
<p>Your changes have been saved successfully.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success modal-close-btn">Great!</button>
</div>
</div>/**
* Example 4: Success Message - from public/showcase/elements/modal.
*/
export function mount(root) {
const successModal = E.modal(root.querySelector('#success-modal'));
$(root).find('#open-success').on('click', () => successModal.open());
}Example 5: Scrollable Content
Modal with long content that scrolls:
Terms and Conditions
1. Introduction
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
2. User Obligations
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
3. Privacy Policy
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
4. Termination
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
5. Liability
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.
<button type="button" class="btn btn-secondary" id="open-scroll">Open Scrollable Modal</button>
<div id="scroll-modal" class="modal">
<div class="modal-header">
<h5 class="modal-title">Terms and Conditions</h5>
<button type="button" class="modal-close">×</button>
</div>
<div class="modal-body modal-body-scrollable">
<h6 class="mb-2">1. Introduction</h6>
<p class="mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.</p>
<h6 class="mb-2">2. User Obligations</h6>
<p class="mb-4">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.</p>
<h6 class="mb-2">3. Privacy Policy</h6>
<p class="mb-4">Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur.</p>
<h6 class="mb-2">4. Termination</h6>
<p class="mb-4">Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.</p>
<h6 class="mb-2">5. Liability</h6>
<p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
laudantium.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-close-btn">Decline</button>
<button type="button" class="btn btn-primary modal-close-btn">Accept</button>
</div>
</div>/**
* Example 5: Scrollable Content - from public/showcase/elements/modal.
*/
export function mount(root) {
const scrollModal = E.modal(root.querySelector('#scroll-modal'));
$(root).find('#open-scroll').on('click', () => scrollModal.open());
}Example 6: Nested Modals
Open a modal from within another modal:
Parent Modal
This is the parent modal.
Child Modal
This modal was opened from the parent modal.
<button type="button" class="btn btn-primary" id="open-parent">Open Parent Modal</button>
<div id="parent-modal" class="modal">
<div class="modal-header">
<h5 class="modal-title">Parent Modal</h5>
<button type="button" class="modal-close">×</button>
</div>
<div class="modal-body">
<p class="mb-3">This is the parent modal.</p>
<button type="button" class="btn btn-secondary" id="open-child">Open Child Modal</button>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-close-btn">Close</button>
</div>
</div>
<div id="child-modal" class="modal">
<div class="modal-header">
<h5 class="modal-title">Child Modal</h5>
<button type="button" class="modal-close">×</button>
</div>
<div class="modal-body">
<p>This modal was opened from the parent modal.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary modal-close-btn">Close Child</button>
</div>
</div>/**
* Example 6: Nested Modals - from public/showcase/elements/modal.
*/
export function mount(root) {
// Nested modals
const parentModal = E.modal(root.querySelector('#parent-modal'));
const childModal = E.modal(root.querySelector('#child-modal'));
$(root).find('#open-parent').on('click', () => parentModal.open());
$(root).find('#open-child').on('click', () => childModal.open());
}Factory Mode - Programmatic Creation
Create modals programmatically without pre-existing HTML using createModal(). Perfect for dynamic content, notifications, and on-demand dialogs.
Example 1: Basic Factory Modal
Create a modal with title, content, and buttons programmatically:
const modal = Domma.elements.createModal({
title: 'Welcome',
content: '<p>This modal was created programmatically!</p>',
size: 'medium',
buttons: [
{id: 'close', text: 'Close', variant: 'primary'}
]
});
modal.open();
<button type="button" class="btn btn-primary" id="demo-factory-basic">Open Factory Modal</button>/**
* Example 1: Basic Factory Modal - from public/showcase/elements/modal.
*/
export function mount(root) {
// Factory Mode Examples
$(root).find('#demo-factory-basic').on('click', () => {
const modal = E.createModal({
title: 'Welcome',
content: '<p>This modal was created programmatically using <code>createModal()</code>!</p><p>No pre-existing HTML required.</p>',
size: 'medium',
buttons: [
{id: 'close', text: 'Close', variant: 'primary'}
]
});
modal.open();
});
}Example 2: Size Variants
Available sizes: small, medium, large, xl:
Domma.elements.createModal({
title: 'Large Modal',
content: '<p>This is a large modal for extensive content.</p>',
size: 'large'
}).open();
<button type="button" class="btn btn-secondary" id="demo-factory-small">Small</button>
<button type="button" class="btn btn-secondary" id="demo-factory-medium">Medium</button>
<button type="button" class="btn btn-secondary" id="demo-factory-large">Large</button>
<button type="button" class="btn btn-secondary" id="demo-factory-xl">Extra Large</button>/**
* Example 2: Size Variants - from public/showcase/elements/modal.
*/
export function mount(root) {
// Size variants
$(root).find('#demo-factory-small').on('click', () => {
E.createModal({
title: 'Small Modal',
content: '<p>This is a small modal (400px max-width).</p>',
size: 'small'
}).open();
});
$(root).find('#demo-factory-medium').on('click', () => {
E.createModal({
title: 'Medium Modal',
content: '<p>This is a medium modal (600px max-width).</p>',
size: 'medium'
}).open();
});
$(root).find('#demo-factory-large').on('click', () => {
E.createModal({
title: 'Large Modal',
content: '<p>This is a large modal (800px max-width).</p>',
size: 'large'
}).open();
});
$(root).find('#demo-factory-xl').on('click', () => {
E.createModal({
title: 'Extra Large Modal',
content: '<p>This is an extra large modal (1000px max-width).</p>',
size: 'xl'
}).open();
});
}Example 3: Custom Buttons with Actions
Define multiple buttons with custom actions:
Domma.elements.createModal({
title: 'Custom Actions',
content: '<p>Choose an action to perform:</p>',
buttons: [
{id: 'cancel', text: 'Cancel', variant: 'secondary'},
{id: 'save', text: 'Save', variant: 'primary'},
{id: 'delete', text: 'Delete', variant: 'danger'}
],
onButtonClick: (buttonId, modal) => {
console.log('Button clicked:', buttonId);
if (buttonId === 'delete') {
Domma.elements.toast('Deleted!', {type: 'success'});
}
}
}).open();
<button type="button" class="btn btn-primary" id="demo-factory-actions">Custom Buttons Modal</button>/**
* Example 3: Custom Buttons with Actions - from public/showcase/elements/modal.
*/
export function mount(root) {
// Custom buttons with actions
$(root).find('#demo-factory-actions').on('click', () => {
E.createModal({
title: 'Custom Actions',
content: '<p>Choose an action to perform:</p>',
buttons: [
{id: 'cancel', text: 'Cancel', variant: 'secondary'},
{id: 'save', text: 'Save', variant: 'primary'},
{id: 'delete', text: 'Delete', variant: 'danger'}
],
onButtonClick: (buttonId, modal) => {
console.log('Button clicked:', buttonId);
if (buttonId === 'save') {
E.toast('Saved successfully!', {type: 'success'});
} else if (buttonId === 'delete') {
E.toast('Deleted!', {type: 'error'});
}
}
}).open();
});
}Example 4: Dynamic Content Loading
Load content asynchronously and display in modal:
async function showUserProfile(userId) {
const modal = Domma.elements.createModal({
title: 'User Profile',
content: '<div class="text-center"><p>Loading...</p></div>',
size: 'large'
});
modal.open();
// Fetch user data
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
// Update modal content
const content = `
<h4>${user.name}</h4>
<p>Email: ${user.email}</p>
<p>Role: ${user.role}</p>
`;
$('#' + modal.element.id + ' .dm-dialog-body').html(content);
}
<button type="button" class="btn btn-primary" id="demo-factory-dynamic">Load Dynamic Content</button>/**
* Example 4: Dynamic Content Loading - from public/showcase/elements/modal.
*/
export function mount(root) {
// Dynamic content loading
$(root).find('#demo-factory-dynamic').on('click', async () => {
const modal = E.createModal({
title: 'User Profile',
content: '<div class="text-center"><p>Loading user data...</p></div>',
size: 'large'
});
modal.open();
// Simulate async data fetch
await new Promise(resolve => setTimeout(resolve, 1000));
const userData = {
name: 'John Doe',
email: 'john.doe@example.com',
role: 'Administrator',
lastLogin: D().format('DD/MM/YYYY')
};
// Update modal content
const content = `
<div class="grid grid-cols-2 gap-4">
<div class="form-group">
<label class="form-label">Name</label>
<p class="mb-0">${userData.name}</p>
</div>
<div class="form-group">
<label class="form-label">Email</label>
<p class="mb-0">${userData.email}</p>
</div>
<div class="form-group">
<label class="form-label">Role</label>
<p class="mb-0">${userData.role}</p>
</div>
<div class="form-group">
<label class="form-label">Last Login</label>
<p class="mb-0">${userData.lastLogin}</p>
</div>
</div>
`;
$(modal.element).find('.dm-dialog-body').html(content);
});
}Example 5: Scrollable Content
Create modal with scrollable body for long content:
Domma.elements.createModal({
title: 'Privacy Policy',
content: `<p>Long content...</p>`.repeat(20),
size: 'large',
scrollable: true,
buttons: [{id: 'accept', text: 'Accept', variant: 'primary'}]
}).open();
<button type="button" class="btn btn-secondary" id="demo-factory-scroll">Scrollable Content</button>/**
* Example 5: Scrollable Content - from public/showcase/elements/modal.
*/
export function mount(root) {
// Scrollable content
$(root).find('#demo-factory-scroll').on('click', () => {
const longContent = Array(15).fill(null).map((_, i) => `
<h6 class="mb-2">Section ${i + 1}</h6>
<p class="mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>
`).join('');
E.createModal({
title: 'Privacy Policy',
content: longContent,
size: 'large',
scrollable: true,
buttons: [
{id: 'decline', text: 'Decline', variant: 'secondary'},
{id: 'accept', text: 'Accept', variant: 'primary'}
],
onButtonClick: (buttonId) => {
if (buttonId === 'accept') {
E.toast('Privacy policy accepted', {type: 'success'});
}
}
}).open();
});
}Promise Mode - Async/Await
Use showModal() for promise-based modals that resolve with the button ID clicked. Perfect for confirmations, choices, and async workflows.
Example 1: Confirmation Dialog
Wait for user confirmation with async/await:
async function deleteItem() {
const result = await Domma.elements.showModal({
title: 'Confirm Deletion',
content: '<p>Are you sure you want to delete this item?</p>',
buttons: [
{id: 'cancel', text: 'Cancel', variant: 'secondary'},
{id: 'delete', text: 'Delete', variant: 'danger'}
]
});
if (result === 'delete') {
console.log('Item deleted!');
Domma.elements.toast('Item deleted successfully', {type: 'success'});
} else {
console.log('Deletion cancelled');
}
}
<button type="button" class="btn btn-danger" id="demo-promise-confirm">Delete Item (Promise)</button>
<div id="promise-result-1" class="mt-2"></div>/**
* Example 1: Confirmation Dialog - from public/showcase/elements/modal.
*/
export function mount(root) {
// Promise Mode Examples
$(root).find('#demo-promise-confirm').on('click', async () => {
const result = await E.showModal({
title: 'Confirm Deletion',
content: '<p>Are you sure you want to delete this item?</p><p class="text-sm text-danger">This action cannot be undone.</p>',
buttons: [
{id: 'cancel', text: 'Cancel', variant: 'secondary'},
{id: 'delete', text: 'Delete', variant: 'danger'}
]
});
const resultDiv = $(root).find('#promise-result-1');
if (result === 'delete') {
resultDiv.html('<span class="text-danger">✓ Item deleted at ' + D().format('HH:mm:ss') + '</span>');
E.toast('Item deleted successfully', {type: 'success'});
} else {
resultDiv.html('<span class="text-secondary">Deletion cancelled</span>');
}
});
}Example 2: Multiple Choice Selection
Let user choose from multiple options:
async function selectSize() {
const size = await Domma.elements.showModal({
title: 'Select Size',
content: '<p>Choose your preferred size:</p>',
buttons: [
{id: 'small', text: 'Small'},
{id: 'medium', text: 'Medium'},
{id: 'large', text: 'Large'},
{id: 'xl', text: 'Extra Large'}
]
});
console.log('Selected size:', size);
}
<button type="button" class="btn btn-primary" id="demo-promise-choice">Select Size</button>
<div id="promise-result-2" class="mt-2">
<span class="text-success" data-if="size.value">✓ Selected: <strong data-bind-text="sizeLabel.value"></strong></span>
</div>/**
* Example 2: Multiple Choice Selection - from public/showcase/elements/modal.
*/
export function mount(root) {
// Multiple choice - the chosen size is an observable the result line binds to
const size = M.observable('');
const sizeLabel = M.computed(() => size.value.toUpperCase());
const handle = M.applyBindings({size, sizeLabel}, root.querySelector('#promise-result-2'));
$(root).find('#demo-promise-choice').on('click', async () => {
const choice = await E.showModal({
title: 'Select Size',
content: '<p>Choose your preferred size:</p>',
buttons: [
{id: 'small', text: 'Small'},
{id: 'medium', text: 'Medium'},
{id: 'large', text: 'Large'},
{id: 'xl', text: 'Extra Large'}
]
});
size.value = choice;
console.log('Selected size:', choice);
});
return () => handle.dispose();
}Example 3: Form Submission with Promise
Combine form input with promise resolution:
async function getUserInput() {
const modal = Domma.elements.createModal({
title: 'Enter Details',
content: `
<div class="form-group">
<label class="form-label">Name</label>
<input type="text" id="user-name" class="form-input">
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" id="user-email" class="form-input">
</div>
`,
buttons: [
{id: 'cancel', text: 'Cancel', variant: 'secondary'},
{id: 'submit', text: 'Submit', variant: 'primary'}
]
});
return new Promise((resolve) => {
modal.options.onButtonClick = (buttonId) => {
if (buttonId === 'submit') {
const data = {
name: $('#user-name').val(),
email: $('#user-email').val()
};
resolve(data);
} else {
resolve(null);
}
};
modal.open();
});
}
// Usage
const userData = await getUserInput();
if (userData) {
console.log('User data:', userData);
}
<button type="button" class="btn btn-primary" id="demo-promise-form">Get User Input</button>
<div id="promise-result-3" class="mt-2">
<span class="text-success" data-if="outcome.value === 'submitted'">✓ Submitted: <span data-bind-text="user.value.name"></span> (<span data-bind-text="user.value.email"></span>)</span>
<span class="text-secondary" data-if="outcome.value === 'cancelled'">Form submission cancelled</span>
</div>/**
* Example 3: Form Submission with Promise - from public/showcase/elements/modal.
*/
export function mount(root) {
// Form submission with promise
// The result line binds to these observables (data-if / data-bind-text in demo.html)
const outcome = M.observable('');
const user = M.observable({name: '', email: ''});
const handle = M.applyBindings({outcome, user}, root.querySelector('#promise-result-3'));
$(root).find('#demo-promise-form').on('click', async () => {
// The answer is a promise: Save resolves it with the form's data; Cancel, the close
// button or the backdrop all go through close(), which resolves it with null.
// Whichever happens first wins. F.modal builds the fields from a blueprint, with
// validation, so Save only fires with a name and a valid email.
const userData = await new Promise((resolve) => {
const modal = F.modal({
name: {type: 'string', label: 'Name', required: true, formConfig: {placeholder: 'Ada Lovelace'}},
email: {type: 'email', label: 'Email', required: true, formConfig: {placeholder: 'ada@example.com'}}
}, {}, {
title: 'Enter Details',
saveText: 'Submit',
onSave: (data) => resolve(data)
});
const close = modal.close.bind(modal);
modal.close = () => { resolve(null); close(); };
modal.open();
});
if (userData && userData.name && userData.email) {
user.value = userData;
outcome.value = 'submitted';
} else {
outcome.value = 'cancelled';
}
});
return () => handle.dispose();
}Example 4: Chained Confirmations
Create multi-step confirmation flows:
async function dangerousOperation() {
// First confirmation
const firstConfirm = await Domma.elements.showModal({
title: 'Warning',
content: '<p>This will permanently delete all data.</p>',
buttons: [
{id: 'cancel', text: 'Cancel', variant: 'secondary'},
{id: 'continue', text: 'Continue', variant: 'warning'}
]
});
if (firstConfirm !== 'continue') return;
// Second confirmation
const secondConfirm = await Domma.elements.showModal({
title: 'Final Confirmation',
content: '<p class="text-danger"><strong>Are you absolutely sure?</strong></p>',
buttons: [
{id: 'no', text: 'No, Go Back', variant: 'secondary'},
{id: 'yes', text: 'Yes, Delete Everything', variant: 'danger'}
]
});
if (secondConfirm === 'yes') {
console.log('Performing dangerous operation...');
Domma.elements.toast('All data deleted', {type: 'error'});
}
}
<button type="button" class="btn btn-danger" id="demo-promise-chain">Dangerous Operation</button>
<div id="promise-result-4" class="mt-2"></div>/**
* Example 4: Chained Confirmations - from public/showcase/elements/modal.
*/
export function mount(root) {
// Chained confirmations
$(root).find('#demo-promise-chain').on('click', async () => {
// First confirmation
const firstConfirm = await E.showModal({
title: 'Warning',
content: '<p class="mb-2">This will permanently delete all data.</p><p class="text-warning"><strong>Are you sure you want to continue?</strong></p>',
buttons: [
{id: 'cancel', text: 'Cancel', variant: 'secondary'},
{id: 'continue', text: 'Continue', variant: 'warning'}
]
});
if (firstConfirm !== 'continue') {
$(root).find('#promise-result-4').html('<span class="text-secondary">Operation cancelled at step 1</span>');
return;
}
// Second confirmation
const secondConfirm = await E.showModal({
title: 'Final Confirmation',
content: '<p class="text-danger"><strong>Are you absolutely sure?</strong></p><p class="text-sm">This action cannot be undone!</p>',
buttons: [
{id: 'no', text: 'No, Go Back', variant: 'secondary'},
{id: 'yes', text: 'Yes, Delete Everything', variant: 'danger'}
]
});
const resultDiv = $(root).find('#promise-result-4');
if (secondConfirm === 'yes') {
resultDiv.html('<span class="text-danger">✓ All data deleted at ' + D().format('HH:mm:ss') + '</span>');
E.toast('All data deleted', {type: 'error'});
} else {
resultDiv.html('<span class="text-secondary">Operation cancelled at step 2</span>');
}
});
}Best Practices
Accessibility
- Focus Management: Automatically focus the first interactive element when modal opens, return focus to trigger when closing
- Keyboard Navigation: Support Escape key to close (use
keyboard: true), Tab to cycle through focusable elements - ARIA Attributes: Add
role="dialog",aria-modal="true",aria-labelledbypointing to title - Screen Readers: Announce modal opening, provide context about purpose and available actions
- Backdrop Semantics: Ensure backdrop is properly marked and doesn't interfere with screen reader navigation
- Close Button: Always provide visible close button with descriptive
aria-label="Close modal"
Performance
- Lazy Content: Load modal content only when opened for heavy forms or images
- Destroy on Close: For one-time modals, call
modal.destroy()after closing to free memory - Limit Nesting: Avoid deeply nested modals (max 2 levels) as it confuses users and impacts performance
- Animation Performance: Use CSS transforms and opacity for smooth 60fps animations
- Body Scroll Lock: Domma automatically prevents body scrolling when modal is open
Common Gotchas
| Issue | Solution |
|---|---|
| Modal appears below other content | Ensure modal has z-index higher than other positioned elements. Default z-index is 1050 for modal, 1040 for backdrop. |
| Backdrop click not working | Check backdropClose: true is set. Ensure backdrop element isn't blocked by modal content extending beyond modal bounds. |
| Modal not closing programmatically | Store modal instance in accessible scope: const modal = Domma.elements.modal('#id'), then call modal.close(). |
| Multiple modals open simultaneously | Manage modal state yourself or use nested modal pattern. Ensure only one primary modal is open at a time for UX clarity. |
| Form submission closing modal | Use e.preventDefault() in form submit handler to prevent default behaviour, then manually close modal after async operations. |
Tips & Tricks
- Auto-Open: Call
modal.open()immediately after creation for splash screens or important announcements - Programmatic Content: Populate modal content dynamically before opening:
$('#modal .modal-body').html(content); modal.open(); - Size Classes: Use
.modal-sm,.modal-lg, or.modal-xlfor different sizes - Loading States: Combine with Loader component for async operations inside modals
- Confirmation Pattern: Return Promise from modal actions for cleaner async/await code
- Mobile Optimization: On small screens, modals automatically use full width with appropriate padding
Config Engine Integration
Use Domma's Config Engine for declarative modal setup.
Single Modal
$.setup({
'#login-modal': {
component: 'modal',
options: {
backdrop: true,
backdropClose: false, // Require explicit action
keyboard: true,
onOpen: () => {
console.log('Login modal opened');
}
}
}
});
Multiple Modals
$.setup({
'#info-modal': {
component: 'modal',
options: {
backdrop: true,
animation: 'fade'
}
},
'#confirm-modal': {
component: 'modal',
options: {
backdropClose: false,
keyboard: false,
onClose: () => {
console.log('User made a choice');
}
}
}
});
With Event Handlers
$.setup({
'#contact-modal': {
component: 'modal',
options: {
backdrop: true,
onOpen: () => {
// Clear form fields
$('#contact-modal form')[0].reset();
},
onClosed: () => {
// Reset any state
console.log('Contact modal closed');
}
},
events: {
submit: (e) => {
e.preventDefault();
// Handle form submission
const formData = new FormData(e.target);
console.log('Submitting:', Object.fromEntries(formData));
}
}
}
});
CSS Customisation
Override these CSS variables to customise Modal appearance and match your design system.
| Variable | Default | Controls |
|---|---|---|
--dm-surface |
var(--dm-white) |
Modal background colour |
--dm-radius-lg |
0.5rem |
Modal corner radius |
--dm-shadow-xl |
0 20px 25px... |
Modal shadow (elevation) |
--dm-transition-normal |
250ms ease |
Open/close animation speed |
--dm-text |
var(--dm-slate-800) |
Modal text colour |
Example Override
:root {
/* Smooth modal animations */
--dm-transition-normal: 400ms cubic-bezier(0.4, 0, 0.2, 1);
/* Dark modal */
--dm-surface: #1e293b;
--dm-text: #f1f5f9;
}
Full CSS Customisation Cheat-Sheet →
Related Elements
Modals work great with these complementary components:
<div class="grid grid-cols-1"><a href="/showcase/elements/dialog" class="card card-hover">
<div class="card-body">
<h4 class="card-title">Dialog</h4>
<p class="card-text">Promise-based alert/confirm/prompt alternatives for simpler
confirmations.</p>
</div>
</a>
<a href="/showcase/elements/toast" class="card card-hover">
<div class="card-body">
<h4 class="card-title">Toast</h4>
<p class="card-text">Non-blocking notifications for feedback after modal actions.</p>
</div>
</a>
<a href="/showcase/elements/forms" class="card card-hover">
<div class="card-body">
<h4 class="card-title">Forms</h4>
<p class="card-text">Form components perfect for modal-based data entry.</p>
</div>
</a>
<a href="/showcase/elements/loader" class="card card-hover">
<div class="card-body">
<h4 class="card-title">Loader</h4>
<p class="card-text">Loading indicators for async operations within modals.</p>
</div>
</a>
</div>