Skip to content

Dialog

Edit profile

Make changes to your profile here. Click save when you are done.

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>

Title

The description sits under the title.

Anything between the header and the footer is the body.

ClassPart
.dialogThe <dialog> element itself
.dialog-closePosition for the corner dismiss control
.dialog-headerTitle area
.dialog-titleHeading — becomes the accessible name
.dialog-descriptionSupporting line — becomes aria-describedby
.dialog-footerA bar across the bottom, with its own surface
AttributeOnMeaning
data-pk-dialog-open="<id>"any controlOpens that dialog as a modal
data-pk-dialog-closea control insideCloses the enclosing dialog
data-pk-dialog-close="<value>"a control insideCloses it and sets returnValue
data-pk-dialog-staticthe dialogClicking the backdrop does not close it
data-state="open" | "closed"the dialogSet by the runtime

.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:

Custom close control

.dialog-close only positions — the control is yours.

The aria-label is not optional on an icon-only control — the icon carries no text.

Leave it out. Esc and the backdrop still dismiss the dialog, so the user is never trapped.

No close button

Escape and the backdrop still dismiss it.

To stop the backdrop dismissing it — when clicking away would lose work — add data-pk-dialog-static:

Unsaved changes

Clicking outside will not close this one.

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.

The panel caps at the viewport height minus a margin and scrolls past that. Nothing to configure.

Terms of service

The panel scrolls once it runs out of room.

Section one. Everything here is ordinary body copy.

Section two. The dialog never grows past the viewport.

Section three. It caps at the screen height minus a margin.

Section four. Past that, this area scrolls.

Section five. Focus stays trapped while you scroll.

Section six. The header scrolls away with the rest.

Section seven.

Section eight.

Section nine.

Section ten.

Rename project

EventCancelableDetail
pk:dialog:before-openyes
pk:dialog:openno
pk:dialog:closeno{ returnValue }
document.getElementById("demo-dialog").addEventListener("pk:dialog:close", (event) => {
if (event.detail.returnValue === "save") save();
});
import { openDialog, closeDialog } from "partialkit";
openDialog("demo-dialog", triggerElement); // triggerElement gets focus back on close
closeDialog("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.

Most of it is the platform’s, but three things are yours:

  • A title. .dialog-title becomes 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.