COMPONENTS / MOLECULES / al-dialog← ALL COMPONENTS

Dialog

<al-dialog><ALDialog> REACTbeta

A modal panel for a focused task or decision, with optional header and footer content. Use the trigger slot to open it and provide a clear heading and dismissal action.

PLAYGROUND2 CONTROLS
PREVIEW / al-dialog / SOUTHLEFT
Loading preview
Delete component

"al-input-stepper" will be removed from the library. This can't be undone.

Close Cancel Delete
FLAGS
CODE — HTML / REACT

Guidance

AUTHORED · CITED TO SOURCE

A modal that interrupts the page: it traps focus, closes on Escape, and returns focus to whatever opened it. It brings its own trigger — slot the control that opens it into the trigger slot and the component wires aria-expanded on it — so the open state lives in one place rather than being synchronised by the page.

WHEN TO USE

  • The user must resolve something before continuing: confirm a destructive action, complete a short required form, acknowledge a consequence.
  • The task is short enough that losing sight of the page behind it costs nothing. A modal is an interruption, and it should be worth interrupting for.
  • You need focus containment. The dialog composes al-focus-trap, so tab order is confined to the dialog while it is open.

WHEN NOT TO USE

  • You are only reporting the outcome of something the user already did. An interruption that carries no decision is a cost with no payoff.USE TOAST →
  • The content is long, or the user needs to refer back to the page while working through it. A panel that slides in beside the page keeps both visible.USE DRAWER →
  • The content is a small amount of supplementary detail attached to one control, revealed on hover or focus.USE TOOLTIP →
  • You are showing a persistent, page-level message about state — an outage, a trial expiring, a validation summary.USE BANNER →

DO

  • Slot the trigger into the trigger slot instead of calling open() from the page. The component sets aria-expanded on the slotted control and stores it as the focus-restore target, and neither happens if you open the dialog from outside.
  • Listen for onDialogClose when you need to react to the dialog closing at all. onDialogCloseButton fires only for the close button — the backdrop click and the Escape key do not reach it.
  • Keep transitionDelay in step with your close animation. It is what delays focus-trap activation, so a value shorter than the transition traps focus before the dialog is visible.
  • Set disableClickOutside for a dialog the user must answer, so a stray click on the backdrop cannot discard a half-finished decision.

DON’T

  • Don't open a dialog from another dialog. Focus restoration targets a single remembered element, and stacking modals produces a focus path the component does not model.
  • Don't rely on the page to restore focus after close(). The component already does it — trigger first, then whatever had focus before the dialog opened — and a second focus call from the page will fight it.
  • Don't use a dialog to present a form long enough to scroll. The user loses the context they were working in and cannot get it back without abandoning the task.
  • Don't leave heading empty. It is what aria-labelledby points at, so an empty heading means the dialog opens with no accessible name.

ACCESSIBILITY IN PRACTICE

  • The container renders role="dialog" with aria-modal and an aria-labelledby generated from the heading, so the dialog announces itself and what it is for on open.
  • Focus returns to the invoking control on EVERY close path — close button, backdrop, Escape, and a programmatic close(). That last case is called out in the source as a WCAG 2.4.3 fix: calling close() from code used to drop focus to <body>.
  • The restore target is resolved in order — the focusable element inside the slotted trigger, then the trigger itself, then whatever had focus before the dialog opened — so focus is not lost when a trigger component does not expose a button.
  • Escape is handled by a shared DialogController rather than a local key listener, so the behaviour is the same one every other overlay in the system uses.

CONTENT GUIDELINES

  • Make the heading state the decision, not the topic: "Delete this project?" tells the user what they are about to do; "Project settings" does not.
  • Label the confirming button with the action it performs — "Delete", "Send invites" — not "OK". The button text is often the only thing read before it is clicked.
  • Say what is irreversible, in the body, in plain words. "This cannot be undone" is worth the line it costs.
  • Keep the body to what the decision needs. Anything the user could read later belongs on the page, not in the interruption.

WHERE THIS CAME FROM

  • libs/al-web-components/components/dialog/dialog.tsFocus restoration on every close path, including the programmatic one that previously dropped focus to the document body.
  • libs/al-web-components/components/dialog/dialog.tsThe dialog role, modal state and generated `aria-labelledby` — the basis for requiring a heading.
  • libs/al-web-components/components/dialog/dialog.tsThe event that fires only for the close button, and the note pointing consumers at `onDialogClose` to catch every path.
  • libs/al-web-components/components/dialog/dialog.tsThe delay before focus-trap activation, and why it has to match the transition.

Each line above names a file and a literal string that must still appear in it.pnpm gate:guidance re-reads them from this built page, so guidance that has stopped being true fails a check rather than misleading you.

NO DESIGN-SYSTEM PROJECT TRACKS THIS COMPONENT AGAINST A FIGMA FILE — IT IS EITHER OUTSIDE EVERY PROJECT’S DECLARED SCOPE, OR NOT YET SEEDED INTO ITS PROJECT’S PARITY MANIFEST.

CHECKEVIDENCEDETAILRESULT
Default stateAXEdialog--DefaultPASS
Advanced statesAXE3 further storiesPASS
Screen readerMANUALNo screen-reader pass has been recorded for this component.NOT RECORDED
Keyboard navigationINTERACTION TESTSNo interaction tests and no manual keyboard pass.NOT RECORDED

MEASURED 2026-09-08 · 4 STORIES · AXE WCAG2A + WCAG2AA + WCAG21A + WCAG21AA + WCAG22AA

COLOR CONTRAST0 RULES FAILING

The CI gate disables color-contrast globally (libs/al-web-components/story-fixture/src/main.ts), so its result never reaches a test run. It is enabled for this measurement, and reported here whichever way it comes out.

NO CONTRAST VIOLATIONS ACROSS THIS COMPONENT’S 4 STORIES.

import '@southleft/al-web-components/components/dialog';
import { ALDialog } from '@southleft/al-react';
PROPTYPEDEFAULT
headingstringHeading text that appears in the header region
isActivebooleanIs active? - **true** Shows the dialog container - **false** Hides the dialog container
ariaLabelledBystringAria Labelled By attribute - Dynamically set for A11y
disableClickOutsidebooleanDisable click outside - **true** Disables closing the dialog on click outside of the dialog container - **false** Enables closing the dialog on click outside of the dialog container
widthnumberThe width of the dialog container - If no value is entered, it defaults to 432px
transitionDelaynumberNumber of ms of the dialog's open/close css transition delay - Used to delay focus trap activation400
dialogTriggeranyThe modal trigger if it not slotted in the 'trigger' slot - Must be set by the trigger's click callback when it calls the modal's open method
SLOTDESCRIPTION
(default)The main body of the dialog
triggerThe trigger that opens/closes the dialog
headerThe header of the dialog that appears above the main slot
footerThe footer of the dialog that appears below the main slot
EVENTDESCRIPTIONTYPE
onDialogOpenFired when the dialog opens. Detail: `{ active, item }` — the new open state and the dialog element itself.CustomEvent
onDialogCloseFired when the dialog closes by any means, including backdrop click and Escape. Detail: `{ active, item }`.CustomEvent
onDialogCloseButtonFired only when the dialog's close button is activated. Detail: `{ active, item }`. Use `onDialogClose` to catch every close path.CustomEvent
METHODDESCRIPTION
setAria()Set aria-expanded to the trigger button 1. Dynamically sets the aria-labelledby for A11y 2. Set isExpanded to this.isActive if it's truthy, otherwise, set it to false
setWidth()Set the width 1. Add a custom property to adjust the width of the dialog container
setBodyOverflow()Set body overflow 1. If the dialog is active, prevent scrolling on the body 2. If the dialog is inactive, allow scrolling on the body
handleOnClickOutside()Handles the click event outside the component: 1. Check if the dialog is active and disableClickOutside is not true 2. Determine if the click occurred inside the active dialog container 3. Check if the click occurred outside the active dialog 4. Close the dialog if the click occurred outside it
handleOnKeydown()Handle on keydown events 1. If the dialog is open and escape is keyed, close the dialog and return focus to the trigger button
handleOnCloseButton()Handle on click of close button 1. Close the dialog 2. Dispatch a custom event on click of close button
sendFocusToTrigger()Send focus to the trigger button that opened the modal 1. Get the trigger that is either an external or slotted element 2. Allow a short timeout for the modal to close 3. Focus the focusable element inside the trigger

SOURCE — components/dialog/dialog.ts · Molecules/Dialog