Dialog
<button class="btn btn-outline" data-pk-dialog-open="demo-dialog">Open dialog</button>
<dialog class="dialog" id="demo-dialog"> <div class="dialog-header"> <h3 class="dialog-title">Edit profile</h3> <p class="dialog-description"> Make changes to your profile here. Click save when you are done. </p> </div>
<div class="field-group"> <div class="field"> <label class="label" for="dialog-name">Name</label> <input class="input" id="dialog-name" value="Ada Lovelace" /> </div> <div class="field"> <label class="label" for="dialog-username">Username</label> <input class="input" id="dialog-username" value="@ada" /> </div> </div>
<div class="dialog-footer"> <button class="btn btn-outline" data-pk-dialog-close>Cancel</button> <button class="btn" data-pk-dialog-close="save">Save changes</button> </div>
<button class="btn btn-ghost btn-icon-sm dialog-close" data-pk-dialog-close> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M18 6 6 18M6 6l12 12"></path> </svg> <span class="sr-only">Close</span> </button></dialog>A dialog is a place to do something, and it can be dismissed. When the user has to answer a question before anything else happens — anything destructive, in particular — use an alert dialog instead.
<button class="btn" data-pk-dialog-open="my-dialog">Open</button>
<dialog class="dialog" id="my-dialog"> <div class="dialog-header"> <h2 class="dialog-title">Title</h2> </div></dialog>Composition
Section titled “Composition”<button class="btn btn-outline" data-pk-dialog-open="composition-dialog">Open</button>
<dialog class="dialog" id="composition-dialog"> <button class="btn btn-ghost btn-icon-sm dialog-close" data-pk-dialog-close aria-label="Close"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M18 6 6 18M6 6l12 12"></path> </svg> </button>
<div class="dialog-header"> <h3 class="dialog-title">Title</h3> <p class="dialog-description">The description sits under the title.</p> </div>
<p class="card-description">Anything between the header and the footer is the body.</p>
<div class="dialog-footer"> <button class="btn btn-outline" data-pk-dialog-close>Cancel</button> <button class="btn" data-pk-dialog-close="save">Save</button> </div></dialog>| Class | Part |
|---|---|
.dialog | The <dialog> element itself |
.dialog-close | Position for the corner dismiss control |
.dialog-header | Title area |
.dialog-title | Heading — becomes the accessible name |
.dialog-description | Supporting line — becomes aria-describedby |
.dialog-footer | A bar across the bottom, with its own surface |
| Attribute | On | Meaning |
|---|---|---|
data-pk-dialog-open="<id>" | any control | Opens that dialog as a modal |
data-pk-dialog-close | a control inside | Closes the enclosing dialog |
data-pk-dialog-close="<value>" | a control inside | Closes it and sets returnValue |
data-pk-dialog-static | the dialog | Clicking the backdrop does not close it |
data-state="open" | "closed" | the dialog | Set by the runtime |
Custom Close Button
Section titled “Custom Close Button”.dialog-close only positions; the control itself is yours. shadcn/ui composes a ghost icon
button, which is what the examples here use:
<button class="btn btn-ghost btn-icon-sm dialog-close" data-pk-dialog-close> <svg …></svg> <span class="sr-only">Close</span></button>.sr-only text and aria-label both name the control. shadcn/ui uses the text node, which has one
advantage: it survives translation tooling that skips attributes.
Anything else works the same way:
<button class="btn btn-outline" data-pk-dialog-open="custom-close">Open</button>
<dialog class="dialog" id="custom-close"> <button class="btn btn-outline btn-xs dialog-close" data-pk-dialog-close> Esc </button>
<div class="dialog-header"> <h3 class="dialog-title">Custom close control</h3> <p class="dialog-description">.dialog-close only positions — the control is yours.</p> </div></dialog>The aria-label is not optional on an icon-only control — the icon carries no text.
No Close Button
Section titled “No Close Button”Leave it out. Esc and the backdrop still dismiss the dialog, so the user is never trapped.
<button class="btn btn-outline" data-pk-dialog-open="no-close">Open</button>
<dialog class="dialog" id="no-close"> <div class="dialog-header"> <h3 class="dialog-title">No close button</h3> <p class="dialog-description">Escape and the backdrop still dismiss it.</p> </div> <div class="dialog-footer"> <button class="btn" data-pk-dialog-close>Done</button> </div></dialog>To stop the backdrop dismissing it — when clicking away would lose work — add
data-pk-dialog-static:
<button class="btn" data-pk-dialog-open="static-dialog">Open static dialog</button>
<dialog class="dialog" id="static-dialog" data-pk-dialog-static> <div class="dialog-header"> <h3 class="dialog-title">Unsaved changes</h3> <p class="dialog-description">Clicking outside will not close this one.</p> </div> <div class="dialog-footer"> <button class="btn" data-pk-dialog-close>Got it</button> </div></dialog>Sticky Footer
Section titled “Sticky Footer”The footer is already full-bleed: it cancels the dialog’s padding, carries a top border and its own surface. In a scrolling dialog it scrolls with the content rather than pinning, which keeps the whole panel one scroll context instead of two.
Scrollable Content
Section titled “Scrollable Content”The panel caps at the viewport height minus a margin and scrolls past that. Nothing to configure.
<button class="btn btn-outline" data-pk-dialog-open="scrollable">Open long dialog</button>
<dialog class="dialog" id="scrollable"> <button class="btn btn-ghost btn-icon-sm dialog-close" data-pk-dialog-close aria-label="Close"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M18 6 6 18M6 6l12 12"></path> </svg> </button>
<div class="dialog-header"> <h3 class="dialog-title">Terms of service</h3> <p class="dialog-description">The panel scrolls once it runs out of room.</p> </div>
<div style="display: grid; gap: 0.75rem"> <p class="card-description">Section one. Everything here is ordinary body copy.</p> <p class="card-description">Section two. The dialog never grows past the viewport.</p> <p class="card-description">Section three. It caps at the screen height minus a margin.</p> <p class="card-description">Section four. Past that, this area scrolls.</p> <p class="card-description">Section five. Focus stays trapped while you scroll.</p> <p class="card-description">Section six. The header scrolls away with the rest.</p> <p class="card-description">Section seven.</p> <p class="card-description">Section eight.</p> <p class="card-description">Section nine.</p> <p class="card-description">Section ten.</p> </div>
<div class="dialog-footer"> <button class="btn btn-outline" data-pk-dialog-close>Decline</button> <button class="btn" data-pk-dialog-close="accept">Accept</button> </div></dialog>With a Form
Section titled “With a Form”<button class="btn" data-pk-dialog-open="form-dialog">Rename project</button>
<dialog class="dialog" id="form-dialog"> <div class="dialog-header"> <h3 class="dialog-title">Rename project</h3> </div> <div class="field"> <label class="label" for="dialog-name">Name</label> <input class="input" id="dialog-name" value="partialkit" /> </div> <div class="dialog-footer"> <button class="btn btn-outline" data-pk-dialog-close>Cancel</button> <button class="btn" data-pk-dialog-close="save">Save</button> </div></dialog>Events
Section titled “Events”| Event | Cancelable | Detail |
|---|---|---|
pk:dialog:before-open | yes | — |
pk:dialog:open | no | — |
pk:dialog:close | no | { returnValue } |
document.getElementById("demo-dialog").addEventListener("pk:dialog:close", (event) => { if (event.detail.returnValue === "save") save();});Programmatic control
Section titled “Programmatic control”import { openDialog, closeDialog } from "partialkit";
openDialog("demo-dialog", triggerElement); // triggerElement gets focus back on closecloseDialog("demo-dialog", "save");Pass the opener when you open a dialog from code. WebKit does not focus a button when it is
clicked, so document.activeElement alone is not a reliable record of where focus should return.
Set dir="rtl" on the page or on any subtree — padding, corners and positions follow the reading
direction on their own. See the button page for a live example.
Accessibility
Section titled “Accessibility”Most of it is the platform’s, but three things are yours:
- A title.
.dialog-titlebecomes the accessible name. A dialog without one is announced as just “dialog”. - A heading level that fits the page, not one picked for its size.
- A name on the close control, since the icon has none.
Everything else — the focus trap, focus returning to whatever opened it, Esc, and the rest of the
page going inert — comes from showModal(), with partialkit filling the gaps where engines differ.