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-dialog and .modal-content wrapper 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();
});
Simplest Modal
<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

Step 6: Complete Working Demo
<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:

Example 1: Small Modal
<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:

Example 2: Large Modal with 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:

Example 3: No Backdrop Close
<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:

Example 4: Success Message
<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:

Example 5: Scrollable Content
<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:

Example 6: Nested Modals
<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();
Example 1: Basic Factory Modal
<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();
Example 2: Size Variants
<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();
Example 3: Custom Buttons with Actions
<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);
}
Example 4: Dynamic Content Loading
<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();
Example 5: Scrollable Content
<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');
    }
}
Example 1: Confirmation Dialog
<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);
}
Example 2: Multiple Choice Selection
✓ Selected:
<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);
}
Example 3: Form Submission with Promise
✓ Submitted: () Form submission cancelled
<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'});
    }
}
Example 4: Chained Confirmations
<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-labelledby pointing 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-xl for 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:

Related Elements
<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>