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
| Component | Placement | Role |
|---|---|---|
| DialogTrigger | Anywhere on the page — not inside Dialog. | Opens the dialog when clicked. Connects via targetDialogId. |
| Dialog | Separate from the trigger. | Root modal container. Holds all inner components. Connects via dialogId. |
| DialogTitle | Inside Dialog. | Provides accessible naming via aria-labelledby. Renders as a heading (<h2> by default). |
| DialogDescription | Inside Dialog. | Provides accessible context via aria-describedby. Renders as <p> by default. |
| DialogClose | Inside Dialog. | Closes the active dialog when clicked. |
Connection Rule
DialogTrigger and Dialog connect by ID — they are never nested:
Dialogusesidentity.dialogIdto identify itself.DialogTriggerusestargeting.targetDialogIdto 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
<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="you@example.com" />
<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").
<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}×{/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
| Prop | Type | Default | Description |
|---|---|---|---|
dialogId | string | "" | Unique identifier for this dialog. |
Settings Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | false | Opens the dialog automatically when the runtime initializes. Useful for announcement or onboarding dialogs that should appear on page load. |
closeOnEscape | boolean | true | Whether pressing |
closeOnOutsideClick | boolean | true | Whether clicking the overlay backdrop closes the active dialog. |
trapFocus | boolean | true | Keeps keyboard focus trapped inside the open dialog. Tab and Shift+Tab cycle through focusable elements within the dialog only. |
preventScroll | boolean | true | Prevents background page scrolling while the dialog is open. |
restoreFocus | boolean | true | 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. |
initialFocusSelector | string | "" | 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
| Prop | Type | Default | Description |
|---|---|---|---|
class | class | ome-dialog-default | CSS class applied to the dialog surface element. |
Placement Options
| Placement | Behavior |
|---|---|
center | Centered both horizontally and vertically. Default modal position. |
top | Aligned to the top edge, centered horizontally. |
bottom | Aligned to the bottom edge, centered horizontally. |
left | Aligned to the left edge, centered vertically. |
right | Aligned to the right edge, centered vertically. |
bottom-center | Same as bottom. |
bottom-left | Bottom-left corner of the viewport. |
bottom-right | Bottom-right corner of the viewport. |
CSS Custom Properties
These properties are available on the dialog surface element (.ome-dialog-default) for runtime customization:
| Property | Default | Description |
|---|---|---|
--ome-dialog-overlay-color | rgba(0,0,0,0.5) | Background color of the overlay backdrop. |
--ome-dialog-offset-x | 0px | Horizontal offset from the computed placement position. |
--ome-dialog-offset-y | 0px | Vertical offset from the computed placement position. |
Keyboard Behavior
| Key | Action |
|---|---|
Escape | Closes the dialog (when closeOnEscape is true). |
Tab | Moves focus to the next focusable element inside the dialog (when trapFocus is true). |
Shift+Tab | Moves 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"andaria-modal="true". aria-labelledbyis auto-wired to theDialogTitleelement's generated ID.aria-describedbyis auto-wired to theDialogDescriptionelement'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.bodywhile 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
| Prop | Type | Default | Description |
|---|---|---|---|
targetDialogId | string | "" | ID of the dialog this trigger should open. Must match the |
Content Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "" | Fallback trigger label used when the trigger slot is empty. Renders as a plain text button. |
Settings Props
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Disables the trigger so it cannot open the dialog. |
Styling Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | class | ome-dialog-default__trigger | CSS class applied to the trigger button element. |
Accessibility
- Triggers get
aria-haspopup="dialog",aria-expanded, andaria-controlswired automatically. - Pressing
EnterorSpaceon 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
| Prop | Type | Default | Description |
|---|---|---|---|
tag | string | h2 | HTML tag used for the title element. Common values: |
Styling Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | class | ome-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
| Prop | Type | Default | Description |
|---|---|---|---|
tag | string | p | HTML tag used for the description element. |
Styling Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | class | ome-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
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Disables the close button so users cannot click it to dismiss the dialog. |
Styling Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | class | ome-dialog-default__close | CSS class applied to the close button element. |
Common Mistakes
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.
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.
Each dialog must have a unique dialogId. If two dialogs share the same ID, only the first one registers — the second will never open.
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.