Skip to main content

Dialog

Overview

Use the Dialog family when you need a modal overlay — confirmations, forms, product previews, alerts, or any content that demands focused user attention. The family is composed of five components that work together: the root Dialog holds the modal content, DialogTrigger opens it from anywhere on the page, and DialogTitle, DialogDescription, and DialogClose provide structure and accessibility inside.

Dialogs are unique among Etch components because the trigger and the dialog are siblings, not parent and child. They connect via a shared ID — identity.dialogId on the Dialog must match targeting.targetDialogId on the trigger. When opened, the dialog node is physically moved into a shared runtime host at the end of document.body, then restored to its authored position on close.

Authoring Structure

DialogTrigger (separate from Dialog — anywhere on the page)
Dialog (the modal content)
├── DialogTitle
├── DialogDescription
├── (your content)
└── DialogClose

Placement Rules

ComponentPlacementRole
DialogTriggerAnywhere on the page — not inside Dialog.Opens the dialog when clicked. Connects via targetDialogId.
DialogSeparate from the trigger.Root modal container. Holds all inner components. Connects via dialogId.
DialogTitleInside Dialog.Provides accessible naming via aria-labelledby. Renders as a heading (<h2> by default).
DialogDescriptionInside Dialog.Provides accessible context via aria-describedby. Renders as <p> by default.
DialogCloseInside Dialog.Closes the active dialog when clicked.

Connection Rule

DialogTrigger and Dialog connect by ID — they are never nested:

  • Dialog uses identity.dialogId to identify itself.
  • DialogTrigger uses targeting.targetDialogId to specify which dialog it opens.
  • These values must match exactly for the connection to work.
  • Multiple triggers can target the same dialogId.
  • Each dialog on a page must have a unique dialogId.

Quick Start

Basic newsletter modal
<OmeDialogTrigger
targeting='{{"targetDialogId":"newsletter"}}'
styling='{{"class":"btn"}}'
>
{#slot default}Subscribe{/slot}
</OmeDialogTrigger>

<OmeDialog
identity='{{"dialogId":"newsletter"}}'
settings='{{"placement":"center"}}'
>
{#slot default}
<OmeDialogTitle>Stay updated</OmeDialogTitle>
<OmeDialogDescription>Get the latest news delivered to your inbox.</OmeDialogDescription>
<form>
<input type="email" placeholder="[email protected]" />
<button type="submit">Subscribe</button>
</form>
<OmeDialogClose>Close</OmeDialogClose>
{/slot}
</OmeDialog>

The trigger and dialog are siblings — the trigger sits wherever you need it in the page, and the dialog can be authored anywhere. They connect because both reference the same ID ("newsletter").

Bottom-right positioned dialog
<OmeDialogTrigger
targeting='{{"targetDialogId":"quick-view"}}'
content='{{"label":"Quick view"}}'
/>

<OmeDialog
identity='{{"dialogId":"quick-view"}}'
settings='{{"placement":"bottom-right","closeOnOutsideClick":true}}'
styling='{{"class":"ome-dialog-default card"}}'
>
{#slot default}
<OmeDialogTitle structure='{{"tag":"h3"}}'>Product details</OmeDialogTitle>
<OmeDialogDescription>Quick product overview.</OmeDialogDescription>
<p>Product information goes here.</p>
<OmeDialogClose styling='{{"class":"btn--icon"}}'>
{#slot default}&times;{/slot}
</OmeDialogClose>
{/slot}
</OmeDialog>

Use the placement setting to position the dialog anywhere in the viewport. When the trigger slot is empty, content.label renders a fallback text button.


Family Components

Dialog (Root)

The root component holds all modal content and manages dialog behavior: focus trapping, scroll locking, close-on-escape, close-on-outside-click, overlay rendering, and viewport placement. It does not render visible UI itself until opened — only a hidden container. When opened, the runtime physically moves the dialog node into a shared host element at the end of document.body.

Identity Props

PropTypeDefaultDescription
dialogIdstring""

Unique identifier for this dialog. DialogTrigger components use this ID to open the dialog via their targetDialogId prop. Must be unique across the page.

Settings Props

PropTypeDefaultDescription
defaultOpenbooleanfalse

Opens the dialog automatically when the runtime initializes. Useful for announcement or onboarding dialogs that should appear on page load.

closeOnEscapebooleantrue

Whether pressing Escape closes the active dialog.

closeOnOutsideClickbooleantrue

Whether clicking the overlay backdrop closes the active dialog.

trapFocusbooleantrue

Keeps keyboard focus trapped inside the open dialog. Tab and Shift+Tab cycle through focusable elements within the dialog only.

preventScrollbooleantrue

Prevents background page scrolling while the dialog is open.

restoreFocusbooleantrue

Returns focus to the element that opened the dialog (typically the trigger) after the dialog closes.

placement"center" | "top" | "bottom" | "left" | "right" | "bottom-center" | "bottom-left" | "bottom-right"center

Controls where the dialog surface appears inside the viewport. center is the standard modal position. Other values align the dialog to the corresponding edge or corner.

initialFocusSelectorstring""

CSS selector targeting a specific element inside the dialog that should receive focus when it opens. When empty, focus moves to the first focusable element in the dialog.

Styling Props

PropTypeDefaultDescription
classclassome-dialog-default

CSS class applied to the dialog surface element.

Placement Options

PlacementBehavior
centerCentered both horizontally and vertically. Default modal position.
topAligned to the top edge, centered horizontally.
bottomAligned to the bottom edge, centered horizontally.
leftAligned to the left edge, centered vertically.
rightAligned to the right edge, centered vertically.
bottom-centerSame as bottom.
bottom-leftBottom-left corner of the viewport.
bottom-rightBottom-right corner of the viewport.

CSS Custom Properties

These properties are available on the dialog surface element (.ome-dialog-default) for runtime customization:

PropertyDefaultDescription
--ome-dialog-overlay-colorrgba(0,0,0,0.5)Background color of the overlay backdrop.
--ome-dialog-offset-x0pxHorizontal offset from the computed placement position.
--ome-dialog-offset-y0pxVertical offset from the computed placement position.

Keyboard Behavior

KeyAction
EscapeCloses the dialog (when closeOnEscape is true).
TabMoves focus to the next focusable element inside the dialog (when trapFocus is true).
Shift+TabMoves focus to the previous focusable element inside the dialog (when trapFocus is true).

Accessibility

The root component wires up ARIA attributes automatically:

  • The dialog surface gets role="dialog" and aria-modal="true".
  • aria-labelledby is auto-wired to the DialogTitle element's generated ID.
  • aria-describedby is auto-wired to the DialogDescription element's generated ID (if present).
  • IDs are auto-generated when not explicitly set.

Behaviors

  • One dialog at a time. Only one dialog can be active. Opening a new dialog closes the previous one without animation.
  • DOM teleport. The dialog node is physically moved into a shared host element at the end of document.body while open, then restored to its authored DOM position on close. This ensures the dialog overlays all page content regardless of where it was authored.

DialogTrigger

The interactive element users click to open a dialog. DialogTrigger is always placed separately from Dialog — they are siblings in the block tree, not parent and child. They connect via a shared ID: targeting.targetDialogId must match the dialog's identity.dialogId.

Multiple triggers can target the same dialog. When the trigger slot is empty, content.label renders a fallback text button.

Targeting Props

PropTypeDefaultDescription
targetDialogIdstring""

ID of the dialog this trigger should open. Must match the identity.dialogId of a Dialog component on the page.

Content Props

PropTypeDefaultDescription
labelstring""

Fallback trigger label used when the trigger slot is empty. Renders as a plain text button.

Settings Props

PropTypeDefaultDescription
disabledbooleanfalse

Disables the trigger so it cannot open the dialog.

Styling Props

PropTypeDefaultDescription
classclassome-dialog-default__trigger

CSS class applied to the trigger button element.

Accessibility

  • Triggers get aria-haspopup="dialog", aria-expanded, and aria-controls wired automatically.
  • Pressing Enter or Space on a focused trigger opens the target dialog.

DialogTitle

Provides accessible naming for the dialog. The root Dialog auto-wires aria-labelledby to this element's ID, so screen readers announce the title when the dialog opens. Must be placed inside the Dialog slot.

Structure Props

PropTypeDefaultDescription
tagstringh2

HTML tag used for the title element. Common values: "h2", "h3", "h4".

Styling Props

PropTypeDefaultDescription
classclassome-dialog-default__title

CSS class applied to the title element.


DialogDescription

Provides additional context for screen readers. The root Dialog auto-wires aria-describedby to this element's ID. Must be placed inside the Dialog slot.

Structure Props

PropTypeDefaultDescription
tagstringp

HTML tag used for the description element.

Styling Props

PropTypeDefaultDescription
classclassome-dialog-default__description

CSS class applied to the description element.


DialogClose

Button that closes the active dialog. Must be placed inside the Dialog slot. You can include any content in its slot — text, icons, or custom markup.

Settings Props

PropTypeDefaultDescription
disabledbooleanfalse

Disables the close button so users cannot click it to dismiss the dialog.

Styling Props

PropTypeDefaultDescription
classclassome-dialog-default__close

CSS class applied to the close button element.


Common Mistakes

Nesting DialogTrigger inside Dialog

DialogTrigger and Dialog are siblings, not parent and child. The trigger must be placed outside the dialog in the block tree. They connect by matching targetDialogId to dialogId — nesting breaks the connection and the trigger will not work.

Forgetting to set dialogId / targetDialogId

Without matching IDs, the trigger has no way to know which dialog to open. Both identity.dialogId on Dialog and targeting.targetDialogId on DialogTrigger must be set to the same value.

Duplicate dialogIds on the same page

Each dialog must have a unique dialogId. If two dialogs share the same ID, only the first one registers — the second will never open.

Expecting the dialog to appear where it is authored

When a dialog opens, its DOM node is physically moved into a shared runtime host at the end of document.body. It does not render in-place where you authored it. This ensures the overlay appears above all page content.


FAQs

Can I have multiple triggers for one dialog?

Yes. Any number of DialogTrigger components can target the same dialogId. Each trigger independently opens the same dialog. This is useful for pages that offer several entry points to the same form or overlay.

Can I have multiple dialogs on one page?

Yes. Give each dialog a unique dialogId and have its triggers target that specific ID. Only one dialog can be active at a time — opening a new dialog closes the previous one without animation.

Does DialogTitle have to be inside Dialog?

Yes. DialogTitle must be placed inside the Dialog slot. The runtime looks for it there to wire up aria-labelledby. If placed outside, the dialog will not have an accessible name.

What happens to the DOM when a dialog opens?

The dialog's DOM node is physically moved from its authored position into a shared host element appended to document.body. When the dialog closes, the node is moved back to its original position. This teleport ensures the overlay always renders above all other page content.

How do I position the dialog in the viewport?

Use the settings.placement prop on Dialog. Options are center (default), top, bottom, left, right, bottom-center, bottom-left, and bottom-right. For fine-tuning, use the CSS custom properties --ome-dialog-offset-x and --me-dialog-offset-y to shift the dialog from its computed position.

How do I change the overlay backdrop color?

Set the --ome-dialog-overlay-color CSS custom property on the dialog surface element (.ome-dialog-default). The default is rgba(0,0,0,0.5). For a lighter backdrop: --ome-dialog-overlay-color: rgba(0,0,0,0.2). For a solid backdrop: --ome-dialog-overlay-color: rgba(0,0,0,0.85).

Can I auto-focus a specific element when the dialog opens?

Yes. Set settings.initialFocusSelector to a CSS selector matching the element you want focused (e.g. "#email-input"). When empty, focus moves to the first focusable element in the dialog.