Modal
A modal dialog for interrupting flows that require user confirmation or input.
Basic usage
Basic usage
This is a basic dialog example.
Controlled visibility
visible is a controlled attribute: the host (button/JS) sets or removes it, and the component never restores it automatically; after closing, listen for oas-ok / oas-cancel and remove visible.
Controlled visibility (visible)
External buttons set / remove visible to control visibility without relying on the footer buttons.
Controlled / uncontrolled contract: the component is uncontrolled + event-notification. On every user close path (mask / ✕ / Esc / cancel) it removes its own visible attribute and emits oas-close (source / action in detail), so the host visible attribute always reflects the real state and never goes stale. For declarative binding (host state drives visible), sync the state to closed inside an oas-close handler so it is not written back after closing; intercepting oas-before-close with preventDefault() keeps the dialog open with visible untouched.
No footer buttons
No footer buttons
The footer action area is hidden; close via ✕ / Esc / mask only.
Disable mask close
Disable mask close
Clicking the mask won't close it; use the buttons or Esc.
Custom width
Custom width
Specify the dialog width via width, supporting pixels or percentages (e.g. 50%); defaults to 520px when unset.
Vertically centered
Vertically centered
By default the dialog is offset toward the top (100px from the top); adding centered centers it vertically.
Draggable
Draggable
Drag the dialog by its title bar; Esc, mask close, and focus behavior stay unchanged.
Fullscreen dialog
fullscreen makes the dialog fill the viewport (no radius, no margin). Precedence: fullscreen wins over width / centered / draggable — width is ignored, centered has no layout effect, dragging is disabled; Esc / mask close, focus trap, and ARIA behavior stay the same.
Fullscreen dialog
The fullscreen dialog fills the viewport without radius or margin. width / centered are ignored and dragging is disabled; Esc / mask close still work.
Maximizable
maximizable shows a maximize/restore button to the left of the ✕ in the header. Maximizing uses the same semantics as fullscreen (fills the viewport, no radius, width ignored, dragging disabled); clicking again restores the original size. Each toggle fires oas-maximize (detail.maximized carries the state). When the fullscreen attribute is explicitly set, the button is hidden (the host already forces fullscreen).
Maximizable dialog
Click the maximize button in the header to fill the viewport, click again to restore. Each toggle fires oas-maximize (toast feedback at the top).
Imperative confirm
The imperative confirm() API is Promise-based and reuses oas-modal (returns Promise<void>; resolves on OK, rejects on cancel). Pass an async onOk callback for a loading confirmation: clicking OK puts the OK button into loading (spinner, no repeated triggers); on resolve the dialog closes and the outer promise resolves; on reject the loading clears and the dialog stays open for retry or cancel.
Imperative confirm
Imperative modal API
The imperative modal.confirm / info / success / warning / error API returns a { close() } handle and reuses oas-modal under the hood. Options: title, content (plain text, no HTML injection), okText, cancelText, onOk, onCancel. When onOk returns a Promise, clicking OK puts the OK button into loading (spinner, no repeated triggers); it closes on resolve, and on reject the loading clears and the dialog stays open for retry or cancel. The cancel button / ✕ / mask / Esc call onCancel and close; the close() handle closes programmatically without firing onCancel. Multiple instances stack independently; destroyAllModal() closes them all at once. Mounts to the nearest oas-app container (falls back to body).
Basic confirm
Semantic variants
Async onOk loading
Multiple instances & destroy all
Declarative semantic variants
The attributes used internally by the imperative module — type / ok-text / cancel-text / no-cancel / focus-ok — are all public oas-modal attributes and can be used declaratively.
Declarative semantic variants
type renders the semantic icon; ok-text/cancel-text customize button labels; focus-ok focuses the "OK" button on open.
no-cancel hides the cancel button, leaving only "OK" in the footer.
Event feedback
Event feedback
Click "OK" or "Cancel" and watch the message at the top-right.
Prompt input
The imperative modal.prompt(options) shows a dialog with an input control, auto-focusing the input on open, and resolves { value, action } (confirm on OK; cancel for cancel / ✕ / mask / Esc). Supports inputValue / placeholder / inputType (text / password / number / textarea) / inputPattern (string regex validation, pattern runs before validator) / validator (true passes, false uses the default message, a string is the error message). Failed validation keeps the dialog open with the error shown; correcting the input clears the error so it can be submitted again. An async onOk puts the OK button into loading.
Prompt capability ships with the main entry: the main entry
@oas-ui/ui/feedback/modalalready bundles the prompt capability (import registers it) — no explicit import needed. If you want a lean core without the prompt machinery (zero cost for confirm/alert-only consumers), import the pure-core entry@oas-ui/ui/feedback/modal/coreinstead — it omits the capability, somodal.promptreturnsnulland warns once in the dev console; explicitly import the capability sub-path there:tsimport '@oas-ui/ui/feedback/modal/prompt'or switch back to the main entry. The full entry
@oas-ui/uiand the CDN feedback family bundle include it too.
Basic prompt
Prompt advanced (textarea / password + pattern)
Body scroll lock
Opening locks body scrolling (overflow hidden + scrollbar-width compensation to prevent layout shift); closing restores it. Multiple instances share a depth counter — the lock releases only when the last one closes. no-scroll-lock opts out.
Body scroll lock
While open, page scrolling is locked and restored on close. Try scrolling the page with the dialog open — the content stays put.
Close interception (before-close)
Listen to the cancelable oas-before-close (event.detail.source identifies the source: ok / cancel / close-btn / mask / esc); calling preventDefault() blocks the close — useful for protecting unsaved data. For imperative dialogs, the OK path is governed by the onOk Promise (closes on resolve); cancel-type paths can still be intercepted.
Close interception
Cancel / ✕ / mask / Esc are all intercepted with a warning; "Save" closes normally.
Three close-entry switches
no-esc-close (disable Esc), no-mask-close (disable mask click), no-close-btn (hide ✕) — each close entry is independently configurable, handing control to the host.
Close-entry switches
Esc and ✕ are disabled (mask click still works); the only close entry is the footer button.
Custom footer (footer slot)
When slot="footer" has content, the built-in OK/Cancel buttons are hidden automatically and the footer is fully owned by the slot.
Footer slot
The footer is fully owned by the slot (built-in buttons auto-hide).
Runtime update (handle.update)
handle.update(partialOptions) incrementally updates title / content / button labels at runtime without touching bound callbacks.
handle.update()
Imperative alertdialog semantics
Imperative confirm / prompt dialogs use role="alertdialog" (screen readers announce immediately); declarative <oas-modal> stays dialog by default and can be overridden with the role attribute.
alertdialog semantics
role="alertdialog": screen readers interrupt current announcements; declarative defaults to dialog.
Mask styling & top positioning
The mask background uses the --oas-modal-mask-bg variable (falls back to the overlay token); --oas-modal-mask-blur enables optional backdrop blur. position="top" snaps the dialog to the top edge of the viewport (default is 100px from the top; centered centers it).
Mask styling / position top
Mask background comes from --oas-modal-mask-bg (here: red 18% + 2px blur).
position="top" snaps the dialog to the top edge; the default is 100px from the top (`centered` centers it).
Layering (z-index)
z-index explicitly overrides the default modal tier (--oas-z-modal): the value stacks on top of --oas-z-index-base (the config-provider floating-layer base), with the dialog always 1 above its mask; unset means the default tier. Use it to precisely control stacking across multiple open instances.
Layer override (z-index)
This dialog uses the default tier. Click the button above to stack a dialog with an explicit z-index and observe the layering.
z-index="3000": mask and dialog are explicitly lifted (dialog still 1 above the mask), covering the base dialog; Esc closes the topmost first.
Close-source event
oas-close carries detail.source (ok / cancel / close-btn / mask / esc / programmatic) and detail.action (confirm / cancel / close); non-OK paths keep emitting oas-cancel for backwards compatibility.
Close source
Close via OK / Cancel / ✕ / mask / Esc and watch the source message at the top-right.
Open/close animation
Opening / closing plays a fade + scale animation by default (transform/opacity only; disabled automatically under prefers-reduced-motion). oas-open fires when opening starts; oas-after-open / oas-after-close fire after the animation ends — imperative dialogs rely on that to unmount only after the animation. The transition attribute picks a preset: zoom (default, fade + scale) / fade (opacity only) / none (no transition, instant show/hide).
Open/close animation + lifecycle events
Default fade + scale animation; the messages show oas-after-open / oas-after-close (fired only after the animation ends).
transition presets (fade / none)
transition="fade": opacity-only transition without scaling.
transition="none": instant show / hide (for performance or fallback scenarios).
Click-position animation origin
At the instant of opening the pointer position is recorded and the dialog scales out from that point (transform-origin points at the click); keyboard / programmatic opens fall back to the center.
Click-position animation origin
The scale animation origin follows the most recent click: it expands from the button you clicked instead of the center.
Non-modal (no-mask)
no-mask: no backdrop is rendered + the focus trap is disabled + focus is not stolen on open (semantics aligned with the native <dialog>.show(), for notifications / canvas helpers and other non-blocking scenarios). Close channels stay (✕ / footer buttons / Esc; pair with no-esc-close to disable Esc).
no-mask non-modal
No backdrop and no focus trap — the rest of the page stays interactive (try clicking page buttons while it is open).
Drag clamping (keep in viewport)
Draggable dialogs have their coordinates clamped inside the viewport (on by default): even large drags cannot push the dialog fully off-screen where it can't be recovered.
Drag clamping
Drag by the title bar as far as you like: the dialog stays inside the viewport (right / bottom edges are clamped) and can always be found again.
Declarative trigger
trigger="element-id" binds any page element as the open trigger: clicking it calls setAttribute('visible') (pure sugar — it does not change the controlled model; closing still means the host removes visible).
Declarative trigger
This button has no onclick of its own — the trigger attribute of oas-modal binds the click-to-open automatically.
Size presets & fullscreen breakpoint
size presets: sm (400px) / lg (720px); an explicit width wins. fullscreen-breakpoint="800": the dialog goes fullscreen automatically when the viewport width drops below the threshold (and restores when widened) — narrow-screen adaptation without JS.
Size presets sm / lg
size="sm": 400px wide.
size="lg": 720px wide.
fullscreen-breakpoint
fullscreen-breakpoint="800": below 800px viewport width the dialog fills the screen (no radius, width ignored); widening the viewport restores the regular dialog.
Enter-to-confirm (confirm-on-enter)
confirm-on-enter (explicit opt-in, off by default): pressing Enter triggers "OK" when the dialog contains no text input controls (ignored while loading; when focus is on a button, native activation wins to avoid double-triggering). Great for quick confirmation dialogs.
confirm-on-enter
Click the blank content area so focus leaves the buttons, then press Enter — same as clicking "OK". If the dialog contains an input, Enter is never hijacked.
Shake feedback when close is blocked
When oas-before-close is blocked with preventDefault() (unsaved-data protection, etc.), the dialog plays a short horizontal shake hinting "this path cannot close"; the class is removed after the animation so it can replay.
Shake when close is blocked
This dialog blocks cancel-type closes: clicking Cancel / ✕ / Esc shakes the dialog to signal the block — it does not close.
Custom close icon (close-icon slot)
slot="close-icon" replaces the default ✕ with rich content (the aria-label semantics are kept).
close-icon slot
The close icon at the top-right is owned by the slot (a custom-styled glyph here).
✕Options selection mode
modal.options({ items, type }) shows an imperative dialog with an embedded option group and resolves { value, action }. type: radio (single-select, value is a string) / checkbox (multi-select, array) / toggle (switch group, array); supports disabled items, async onOk loading, and close() / update() handles (same shape as prompt).
options radio / checkbox / toggle
Rendering strategy
destroy-on-close clears the content nodes on close (refill before the next open); append-to mounts the dialog into a target container (escaping host overflow clipping).
destroy-on-close / append-to
Content is cleared on close; the button refills it before the next open.
The dialog is mounted into a body-level portal, so a host with overflow:hidden cannot clip it.
Focus trap & scroll lock switches
initial-focus specifies which element receives focus on open (a CSS selector; the panel is searched first, then light DOM; falls back to the default focus when nothing matches); no-focus-trap turns off the Tab focus trap (for scenarios where the host manages focus itself); no-scroll-lock turns off the body scroll lock (the page keeps scrolling).
initial-focus / no-focus-trap / no-scroll-lock
On open, focus lands directly on the input (initial-focus); no Tab trapping, no page scroll lock.
Media region (media slot)
slot="media" provides a media region at the top of the body (icon / illustration / thumbnail); it is rendered only when it has content — when empty, the region is not rendered and leaves no gap.
media slot
The top media region comes from slot="media"; when no content is provided the region is not rendered and leaves no gap.
Sticky footer (sticky-footer)
sticky-footer makes the body scroll within --oas-modal-body-max-height (default 60vh) while the footer stays pinned to the dialog bottom instead of scrolling away (exempt under fullscreen). Off by default — the layout is identical to before when not set; override the variable on the host to adjust the body scroll height.
sticky-footer
Long content (this block is 90vh tall): the body scrolls within the limited height while the footer stays pinned and visible.
API
Methods
| Method | Description |
|---|---|
modal.confirm({ title?, content?, okText?, cancelText?, onOk?, onCancel? }) | Opens a confirm dialog (OK / Cancel buttons), returns { close } |
modal.info(options) / modal.success(options) / modal.warning(options) / modal.error(options) | Semantic dialog: matching icon + single "OK" button, returns { close } |
modal.prompt({ title?, inputValue?, placeholder?, inputType?, validator?, onOk? }) | Input dialog: resolves { value, action }, returns a Promise & { close, update } |
modal.options({ title?, content?, items?, type?, okText?, cancelText?, onOk? }) | Options dialog (type: radio / checkbox / toggle): resolves { value, action } (radio → a single string; checkbox/toggle → an array), returns a Promise & { close, update } |
destroyAllModal() | Closes and destroys all imperative dialogs (after each close animation ends) |
- Options:
{ title?, content?, okText?, cancelText?, onOk?, onCancel? }.contentis plain text; whenonOkreturns a Promise the OK button enters loading (closes on resolve, stays open on reject for retry or cancel). - Returns a
{ close() }handle: closes the current instance programmatically without firingonOk/onCancel. - Mounts to the nearest
oas-appcontainer (falls back tobody); multiple instances stack; imperative instances unmount only after the close animation ends (oas-after-close).
oas-modal
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
append-to | Portal mount point: moves the mask and dialog into an isolated shadow inside the target container (body or CSS selector, styles stay scoped), escaping host overflow clipping; moved back to the host shadow when the attribute is removed or unmatched | — | — |
cancel-text | Cancel button label; defaults to locale modal.cancel | — | — |
centered | Vertically center the dialog | boolean | — |
confirm-on-enter | When explicitly enabled, Enter triggers OK (topmost modal only; effective only when the dialog has no text input controls and focus is not on a button/link or other native interactive control, preventing double triggers; off by default) | boolean | — |
destroy-on-close | Clear the host children after the close animation finishes (re-rendered on next open) | boolean | — |
draggable | Drag the dialog via its header | boolean | — |
focus-ok | Move focus to the "OK" button on open (default: the "Cancel" button) | boolean | — |
fullscreen | Display fullscreen: the dialog fills the viewport without radius or margin (takes precedence over width / centered / draggable) | boolean | — |
fullscreen-breakpoint | Automatically go fullscreen when the viewport width drops below this threshold (px), recomputed on resize while open; unioned with the explicit fullscreen attribute | — | — |
initial-focus | Focus the element matching this selector on open (dialog first, then host light DOM); falls back to focus-ok / the Cancel button / OK / ✕ | — | — |
loading | Put the OK button into loading state (disabled + spinner), blocking repeated confirms | boolean | — |
maximizable | Show a maximize/restore button in the header: maximizing uses the same semantics as fullscreen; hidden when fullscreen is explicitly set | boolean | — |
no-cancel | Hide the cancel button (the footer keeps only "OK"; built into semantic variants) | boolean | — |
no-close-btn | Hide the ✕ close button in the title bar | boolean | — |
no-esc-close | Disable closing with Esc (Esc is only handled by the topmost visible modal) | boolean | — |
no-focus-trap | Disable the focus trap (Tab no longer cycles inside the dialog) | boolean | — |
no-footer | Hide footer action buttons | boolean | — |
no-mask | Maskless non-modal: no backdrop rendered, focus trap off, no focus steal on open (like HTML dialog.show(); the rest of the page stays interactive) | boolean | — |
no-mask-close | Disable closing on mask click | boolean | — |
no-scroll-lock | Do not lock body scroll while open (the default lock compensates for scrollbar width to prevent layout shift) | boolean | — |
ok-text | OK button label; defaults to locale modal.ok | — | — |
position | Vertical positioning: top pins the dialog to the viewport top edge; default sits 100px from the top; when set together with centered, the top rule wins (later in the cascade) | — | — |
role | Dialog ARIA role (default dialog; set alertdialog for semantic confirmation scenarios) | string | dialog |
size | Size preset: sm (400px) / lg (720px); an explicit width wins; invalid values fall back to the theme default 520px | ModalSizePreset | — |
sticky-footer | Sticky footer: the body scrolls within --oas-modal-body-max-height (default 60vh) while the footer stays pinned to the dialog bottom instead of scrolling away; exempt under fullscreen (data-fullscreen). Off by default — layout is unchanged when not set (non-breaking) | — | — |
title | Title text (rendered into the visible title region; absorbed from the host on read so no native hover tooltip remains; pass an empty string to clear); use the "title" slot for rich content | string | — |
transition | Open/close animation preset: zoom (default, fade + scale, scale origin follows the click position before opening) / fade (opacity only) / none (no transition, instant) | — | — |
trigger | Declarative trigger element id: clicking the element sets visible to open the modal (does not touch the controlled model); silently retries on later updates when the element is missing | — | — |
type | Semantic variant: info/success/warning/error, renders the matching semantic icon above the content | ModalVariant | — |
visible | Whether shown | boolean | — |
width | Dialog width (px or percentage) | — | — |
z-index | Explicit z-index base stacked on --oas-z-index-base (dialog sits 1 above the mask; defaults to the --oas-z-modal tier) | — | — |
Events
| Event | Description |
|---|---|
oas-after-close | Close animation finished (canonical, aligned with the after-* family); no detail. destroy-on-close content clearing happens after this |
oas-after-open | Open animation finished (canonical, aligned with the after-* family); no detail; same as oas-opened |
oas-before-close | Emitted before a close request (OK / cancel / ✕ / mask / Esc), cancelable, detail: { source }; preventDefault blocks the close and shakes the dialog as feedback (programmatic close bypasses the interception) |
oas-cancel | Cancel: cancel button / ✕ / mask click / Esc |
oas-close | Emitted when closing starts; detail: { source, action } (source: ok/cancel/close-btn/mask/esc/programmatic; action: confirm/cancel/close) |
oas-closed | [Compat alias] Close animation finished, same as oas-after-close; will be removed later |
oas-maximize | Fired on maximize/restore toggle, detail: { maximized } |
oas-ok | Clicked "OK" |
oas-open | Emitted when opening starts (after the scroll lock and focus move); no detail |
oas-opened | [Compat alias] Open animation finished, same as oas-after-open; will be removed later |
Slots
| Name | Description |
|---|---|
| default | Dialog body content |
close-icon | Custom close icon (replaces the default ✕) |
description | Description text (shown below the title) |
footer | Footer action area |
media | Media region slot (icon / illustration / thumbnail); centered at the top of the body, rendered only when it has content (no placeholder when empty) |
title | Rich title content slot; overrides the title attribute text when present |
CSS Variables
| CSS Variable | Default |
|---|---|
--oas-glass-blur | none |
--oas-glass-ring | transparent |
--oas-modal-body-max-height | 60vh |
--oas-modal-mask-bg | var(--oas-color-overlay) |
--oas-modal-mask-blur | 0px |
--oas-modal-max-height | 90vh |
--oas-modal-origin-x | center |
--oas-modal-origin-y | center |
role="dialog" + aria-modal="true"; focus moves to the "Cancel" button on open (to the "OK" button with focus-ok) and is restored on close.