Morphos
Overlays

Dialog

An accessible modal dialog with focus trapping and scroll locking.

Interactive example

Installation

npm install @morphos/overlays
pnpm add @morphos/overlays
yarn add @morphos/overlays
bun add @morphos/overlays

Import

import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogTitle,
  DialogDescription,
  DialogClose,
} from '@morphos/overlays'

Usage

@Component()
class MyComponent extends StatefulComponent {
  @State() dialog = new Dialog()

  render() {
    return (
      <>
        <DialogTrigger dialog={this.dialog}>Open</DialogTrigger>
        <DialogContent dialog={this.dialog} aria-labelledby="my-dialog-title">
          <DialogTitle id="my-dialog-title">Confirm</DialogTitle>
          <DialogDescription>Are you sure you want to proceed?</DialogDescription>
          <DialogClose dialog={this.dialog}>Cancel</DialogClose>
          <button onClick={() => this.dialog.closeDialog()}>Confirm</button>
        </DialogContent>
      </>
    )
  }
}

Compound components

ComponentDescription
DialogRoot state manager. Owns open state and exposes openDialog() / closeDialog() / toggle().
DialogTrigger<button> that opens the dialog. Sets aria-haspopup="dialog", aria-expanded, and data-open.
DialogContentRenders into a Portal with a sibling backdrop <div data-morphos-backdrop>. Sets role="dialog" and aria-modal="true". Applies focus trap and scroll lock while open.
DialogTitleRenders as <h2> by default (configurable via as). Pair its id with aria-labelledby on DialogContent.
DialogDescriptionRenders as <p>. Pair its id with aria-describedby on DialogContent.
DialogClose<button> that calls closeDialog().

Props — Dialog

PropTypeDefaultDescription
openbooleanControlled open state. When set, the consumer owns the state.
defaultOpenbooleanfalseInitial open state in uncontrolled mode.
onOpenChange(open: boolean) => voidCalled when the open state changes.
closeOnEscapebooleantrueWhether pressing Escape closes the dialog.
closeOnBackdropClickbooleantrueWhether clicking the backdrop closes the dialog.
childrenChildrenContent — typically DialogTrigger and DialogContent.

Methods

MethodDescription
openDialog()Opens the dialog. Emits onOpenChange(true).
closeDialog()Closes the dialog. Emits onOpenChange(false).
toggle()Toggles the open state. Emits onOpenChange.

Props — DialogTrigger

PropTypeDefaultDescription
dialogDialogThe Dialog instance this trigger controls.
childrenChildrenThe trigger element content.
classstringCSS class.
idstringHTML id.

Props — DialogContent

PropTypeDefaultDescription
dialogDialogThe Dialog instance this content belongs to.
childrenChildrenDialog body content.
classstringCSS class.
idstringHTML id. Falls back to an auto-generated id.
aria-labelstringAccessible label. Use when no DialogTitle is present.
aria-labelledbystringID of the element that labels the dialog (e.g. a DialogTitle).
aria-describedbystringID of the element that describes the dialog (e.g. a DialogDescription).

Props — DialogTitle

PropTypeDefaultDescription
as"h1" | "h2" | "h3" | "h4" | "h5" | "h6""h2"The HTML heading element to render.
childrenChildrenTitle text.
classstringCSS class.
idstringHTML id.

Props — DialogDescription

PropTypeDefaultDescription
childrenChildrenDescription text.
classstringCSS class.
idstringHTML id.

Props — DialogClose

PropTypeDefaultDescription
dialogDialogThe Dialog instance this close button controls.
childrenChildrenButton content.
classstringCSS class.
idstringHTML id.

data-* attributes

AttributeElementWhen present
data-openDialogTriggerDialog is open
data-openDialogContent rootDialog is open

Always provide either aria-label or aria-labelledby on DialogContent. Point aria-labelledby at the id of your DialogTitle for best accessibility.

DialogContent calls trapFocus() and lockScroll() from @morphos/core when it mounts while open (and again whenever the dialog's open state changes). Focus cycles through the focusable elements inside the content element, and the page scroll lock is released automatically when the dialog closes or DialogContent unmounts.

Styling example

[data-morphos-backdrop] {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.4);
}

.morphos-dialog-content {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 100%;
  max-width: 28rem;
  padding: var(--morphos-space-4);
  border-radius: var(--morphos-radius-lg);
}

.morphos-dialog-close {
  position: absolute;
  top: var(--morphos-space-3);
  right: var(--morphos-space-3);
}

On this page