Modal
Centered dialog over a dim mask. Focus stays inside the panel until it closes.
Mask uses color-mix(in srgb, #000 40%, transparent) at z-index 3990. The panel sits at 4000. A second modal stacks +10 per level. Escape and mask clicks apply only to the topmost dialog; closing it restores focus to the previous panel.
Demos
Basic
open is controlled. Dismissals emit update:open with detail: false and close. Unsaved edits will be written to the workspace. You can undo this later.
Width presets
sm 400 / md 520 / lg 720. A number is pixels. At ≤640px the panel uses a 16px side margin and max-height: 85vh. Current width token: md
Dialog confirm
preset="dialog" renders Cancel and Confirm. Confirm emits confirm and does not close by itself. The primary action emits confirm.
Danger confirm
confirm-variant="danger" paints the dialog primary button with the danger fill. It stays a flat footer button, the same chrome as the approved mock. “Booth intro” will be removed permanently.
Form in body
preset="card" is a padded content shell. Put fields in the default slot. Loading confirm
loading shows a spinner on the dialog primary button and ignores confirm clicks. Uploading assets…
Non-closable
closable="false" hides the close button and ignores Escape. Finish this step to continue.
Mask not closable
mask-closable="false" keeps the mask but ignores clicks on it. Escape and the close button still dismiss when closable. Clicking the dim layer does nothing.
Nested modal
This panel stays underneath.
Escape closes only this layer.
Alpine
<div x-data="{ open: false }">
<gk-button type="button" x-on:click="open = true">Open</gk-button>
<gk-modal
title="Hello"
x-bind:open="open"
x-on:update:open="open = $event.detail"
>
<p>Alpine can drive the same element.</p>
</gk-modal>
</div>The string "false" is treated as closed, so attribute bindings from Alpine work.
API
Props
| Prop | Type | Default |
|---|---|---|
open | boolean | false |
title | string | '' |
closable | boolean | true |
mask-closable | boolean | true |
preset | 'dialog' | 'card' | '' | '' |
width | number | 'sm' | 'md' | 'lg' | 'md' (520px) |
loading | boolean | false |
confirm-variant | 'primary' | 'danger' | 'primary' |
confirm-text | string | 'Confirm' |
cancel-text | string | 'Cancel' |
confirm-text and cancel-text label the built-in dialog footer. Slot footer to replace that footer.
Slots
| Name | Description |
|---|---|
| default | Body |
header | Extra header content beside title |
footer | Actions. Replaces the dialog preset buttons when present |
Events
| Name | Detail |
|---|---|
update:open | false when dismissed |
close | Dismissed (Escape, mask, close, cancel) |
confirm | Dialog primary action. Does not close automatically |
CSS parts
mask, panel, header, title, close, body, footer, plus cancel / confirm / spinner on the dialog preset buttons.
Enter and leave use a 180ms opacity and 10px translate. The panel is position: fixed inside the component shadow tree so ::part works. It is not portaled to document.body the way Select and Date Picker panels are.
Stacking
| Level | Mask | Panel |
|---|---|---|
| 0 | 3990 | 4000 |
| 1 | 4000 | 4010 |
Drawers share this blocking stack, so a dialog opened after a drawer paints above it.