Skip to content

Modal ​

A modal dialog for interrupting flows that require user confirmation or input.

Basic usage ​

Basic usage

Open dialog

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)

Open (set visible)Close (remove 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

Open dialog without footer buttons

The footer action area is hidden; close via ✕ / Esc / mask only.

Disable mask close ​

Disable mask close

Open dialog

Clicking the mask won't close it; use the buttons or Esc.

Custom width ​

Custom width

Open dialog with custom width

Specify the dialog width via width, supporting pixels or percentages (e.g. 50%); defaults to 520px when unset.

Vertically centered ​

Vertically centered

Open vertically centered dialog

By default the dialog is offset toward the top (100px from the top); adding centered centers it vertically.

Draggable ​

Draggable

Open draggable dialog

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

Open 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

Open 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

Async confirmLoading 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

Basic confirmCustom labels

Semantic variants

InfoSuccessWarningError

Async onOk loading

Async submit

Multiple instances & destroy all

Open threeDestroy 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

Open success dialogOpen single-button dialog

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

Open and listen to events

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/modal already 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/core instead — it omits the capability, so modal.prompt returns null and warns once in the dev console; explicitly import the capability sub-path there:

ts
import '@oas-ui/ui/feedback/modal/prompt'

or switch back to the main entry. The full entry @oas-ui/ui and the CDN feedback family bundle include it too.

Basic prompt

Basic inputValidation keeps open

Prompt advanced (textarea / password + pattern)

Feedback (textarea)Set 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

Open and lock scrolling

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

Open form dialog

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

Open minimal dialog

Esc and ✕ are disabled (mask click still works); the only close entry is the footer button.

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

Open custom footer

The footer is fully owned by the slot (built-in buttons auto-hide).

CancelSubmit task

Runtime update (handle.update) ​

handle.update(partialOptions) incrementally updates title / content / button labels at runtime without touching bound callbacks.

handle.update()

Open, update after 3s

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

Open alertdialog

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

Custom maskTop position

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)

Open base dialog (default tier)Stack a z-index=3000 dialog on top

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

Open and watch sources

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

Open (watch animation & events)

Default fade + scale animation; the messages show oas-after-open / oas-after-close (fired only after the animation ends).

transition presets (fade / none)

fade (opacity only)none (instant)

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

Click here to open (animates from the button)

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

Open non-modal dialog

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

Open and drag

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

Click me to open (trigger bound)

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=smsize=lg

size="sm": 400px wide.

size="lg": 720px wide.

fullscreen-breakpoint

Open (try narrowing the window below 800px)

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

Open (click the body first, then press 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

Open and try to close

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

Open with a custom close icon

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

RadioCheckboxToggle

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

destroy-on-closeappend-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

Focus input on open

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

Open dialog with media

The top media region comes from slot="media"; when no content is provided the region is not rendered and leaves no gap.

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

Open long-content dialog

Long content (this block is 90vh tall): the body scrolls within the limited height while the footer stays pinned and visible.

API ​

Methods ​

MethodDescription
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? }. content is plain text; when onOk returns 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 firing onOk / onCancel.
  • Mounts to the nearest oas-app container (falls back to body); multiple instances stack; imperative instances unmount only after the close animation ends (oas-after-close).

oas-modal ​

Attributes ​

AttributeDescriptionTypeDefault
append-toPortal 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-textCancel button label; defaults to locale modal.cancel——
centeredVertically center the dialogboolean—
confirm-on-enterWhen 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-closeClear the host children after the close animation finishes (re-rendered on next open)boolean—
draggableDrag the dialog via its headerboolean—
focus-okMove focus to the "OK" button on open (default: the "Cancel" button)boolean—
fullscreenDisplay fullscreen: the dialog fills the viewport without radius or margin (takes precedence over width / centered / draggable)boolean—
fullscreen-breakpointAutomatically go fullscreen when the viewport width drops below this threshold (px), recomputed on resize while open; unioned with the explicit fullscreen attribute——
initial-focusFocus the element matching this selector on open (dialog first, then host light DOM); falls back to focus-ok / the Cancel button / OK / ✕——
loadingPut the OK button into loading state (disabled + spinner), blocking repeated confirmsboolean—
maximizableShow a maximize/restore button in the header: maximizing uses the same semantics as fullscreen; hidden when fullscreen is explicitly setboolean—
no-cancelHide the cancel button (the footer keeps only "OK"; built into semantic variants)boolean—
no-close-btnHide the ✕ close button in the title barboolean—
no-esc-closeDisable closing with Esc (Esc is only handled by the topmost visible modal)boolean—
no-focus-trapDisable the focus trap (Tab no longer cycles inside the dialog)boolean—
no-footerHide footer action buttonsboolean—
no-maskMaskless 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-closeDisable closing on mask clickboolean—
no-scroll-lockDo not lock body scroll while open (the default lock compensates for scrollbar width to prevent layout shift)boolean—
ok-textOK button label; defaults to locale modal.ok——
positionVertical 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)——
roleDialog ARIA role (default dialog; set alertdialog for semantic confirmation scenarios)stringdialog
sizeSize preset: sm (400px) / lg (720px); an explicit width wins; invalid values fall back to the theme default 520pxModalSizePreset—
sticky-footerSticky 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)——
titleTitle 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 contentstring—
transitionOpen/close animation preset: zoom (default, fade + scale, scale origin follows the click position before opening) / fade (opacity only) / none (no transition, instant)——
triggerDeclarative 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——
typeSemantic variant: info/success/warning/error, renders the matching semantic icon above the contentModalVariant—
visibleWhether shownboolean—
widthDialog width (px or percentage)——
z-indexExplicit z-index base stacked on --oas-z-index-base (dialog sits 1 above the mask; defaults to the --oas-z-modal tier)——

Events ​

EventDescription
oas-after-closeClose animation finished (canonical, aligned with the after-* family); no detail. destroy-on-close content clearing happens after this
oas-after-openOpen animation finished (canonical, aligned with the after-* family); no detail; same as oas-opened
oas-before-closeEmitted 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-cancelCancel: cancel button / ✕ / mask click / Esc
oas-closeEmitted 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-maximizeFired on maximize/restore toggle, detail: { maximized }
oas-okClicked "OK"
oas-openEmitted 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 ​

NameDescription
defaultDialog body content
close-iconCustom close icon (replaces the default ✕)
descriptionDescription text (shown below the title)
footerFooter action area
mediaMedia region slot (icon / illustration / thumbnail); centered at the top of the body, rendered only when it has content (no placeholder when empty)
titleRich title content slot; overrides the title attribute text when present

CSS Variables ​

CSS VariableDefault
--oas-glass-blurnone
--oas-glass-ringtransparent
--oas-modal-body-max-height60vh
--oas-modal-mask-bgvar(--oas-color-overlay)
--oas-modal-mask-blur0px
--oas-modal-max-height90vh
--oas-modal-origin-xcenter
--oas-modal-origin-ycenter

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.

Released under the MIT OR Apache-2.0 License.