---
title: "Modals with the LightningModal base class"
url: "https://trailassembly.com/lwc-modals-and-lightning-modal/"
topic: "LWC and Front-End"
published: "2026-09-18T10:39:03+00:00"
updated: "2026-10-06T13:58:06+00:00"
---

# Modals with the LightningModal base class

**The short answer:** extend `LightningModal` rather than building a dialog out of [SLDS](https://trailassembly.com/lwc-css-and-styling/) markup by hand. The base class handles the overlay, the focus trap, the escape key, the ARIA roles and the stacking, and it gives you a promise-based API where opening the modal returns whatever the modal closes with. A hand-rolled `slds-modal` div reproduces the appearance and almost never reproduces the behaviour.

## Writing the modal

A modal is a normal LWC with one difference in the class declaration and three provided layout components in the template.

```apex
// confirmDelete.js
import LightningModal from 'lightning/modal';
import { api } from 'lwc';

export default class ConfirmDelete extends LightningModal {
    @api recordName;

    handleCancel() {
        this.close('cancel');
    }

    handleConfirm() {
        this.close('confirm');
    }
}
```

```apex
<!-- confirmDelete.html -->
<template>
    <lightning-modal-header label="Delete record"></lightning-modal-header>
    <lightning-modal-body>
        <p>Delete {recordName}? This cannot be undone.</p>
    </lightning-modal-body>
    <lightning-modal-footer>
        <lightning-button label="Cancel" onclick={handleCancel}></lightning-button>
        <lightning-button variant="destructive" label="Delete"
            onclick={handleConfirm}></lightning-button>
    </lightning-modal-footer>
</template>
```

You do not write the outer container, the backdrop, or the close button. Those come from the base class. The three section components are optional individually, though a modal with no header needs an accessible name supplied another way.

## Opening it and getting a value back

```apex
import ConfirmDelete from 'c/confirmDelete';

async handleDeleteClick() {
    const result = await ConfirmDelete.open({
        size: 'small',
        description: 'Confirm deletion of the selected record',
        recordName: this.record.Name
    });

    if (result === 'confirm') {
        await this.deleteRecord();
    }
}
```

You import the modal class directly and call the static `open()` method. There is no tag in your template and no `lwc:if` flag to manage, which removes a whole category of state-handling bugs.

`open()` returns a promise that settles when the modal closes. The value is whatever you passed to `this.close()`. A modal dismissed with the escape key or the close button resolves with `undefined`, so treat `undefined` as a cancel and never as a default yes.

Properties passed in the `open()` object are assigned to the modal’s `@api` properties, so `recordName` above must be decorated in the modal class. Reserved keys are `size`, `description`, `label` and `disableClose`.

`size` takes `small`, `medium`, `large` or `full`. `disableClose` removes the close button and blocks the escape key, which you should use sparingly and only when the user genuinely must choose.

## Accessibility, which is the real argument

A dialog has requirements that are easy to state and tedious to implement. The base class covers them.

- Focus moves into the modal when it opens and returns to the triggering element when it closes.

- Tab cycles within the modal and cannot escape into the page behind it.

- The escape key closes it unless you disabled that.

- The content behind is inert, so a screen reader does not read through it.

- The dialog gets `role="dialog"` and `aria-modal`.

What you still owe it is a name and a description. The `label` on `lightning-modal-header` becomes the accessible name, so it must describe the dialog’s purpose rather than say “Dialog”. The `description` passed to `open()` provides the accessible description for assistive technology, and it is separate from anything visible.

Focus placement inside the modal is still yours to think about. The base class puts focus somewhere sensible, but for a form you usually want the first input focused explicitly. Do it in `renderedCallback` behind a one-shot guard, and never put initial focus on a destructive button.

## Practical notes

The modal renders outside your component’s DOM, so your stylesheet does not reach it and your `querySelector` cannot find it. Style the modal in its own CSS file.

Events dispatched from the modal do not bubble up to the component that opened it, because they are not in the same tree. Return data through `close()`. If you need to report progress while the modal is still open, pass a callback function in as a property.

Support depends on the container. Lightning Experience is the primary one. Before relying on `lightning/modal` inside an Experience Cloud site or a custom container, check the current documentation for that context rather than assuming it is available.

A component can open a modal from a quick action, and that combination needs care: closing the action screen while the modal is open leaves an orphaned overlay. Close the modal first.

## Frequently asked

**Can I open a modal from a modal?** Yes, and the stacking is handled. Two levels is usually one too many for the user.

**How do I stop the escape key closing an unsaved form?** Pass `disableClose: true` and provide your own cancel button with a confirmation step.

**Why can I not style the modal from the parent?** It renders in a separate tree. Put the styles in the modal component’s own CSS.

**Is the old slds-modal markup still valid?** It still renders. You would be reimplementing focus trapping and inertness yourself, which is why the base class exists.

---

Source: https://trailassembly.com/lwc-modals-and-lightning-modal/
Trail Assembly — independent Salesforce write-ups.
