Components

Modal

Built on <dialog>. The browser handles the top layer, the backdrop, focus trapping and the escape key.

Delete project
This cannot be undone.

No script is written above. commandfor names the dialog and command says what to do with it.

AttributeMeaning
command="show-modal"open it as a modal
command="close"close it
closedby="any"clicking the backdrop closes it
no closedbyonly the escape key closes it

The surface comes from a card

pui-modal is only a frame: it centers the dialog, sizes it and dims the page. What it looks like is whatever you put inside, which is usually a card. A modal is composed from pieces you already know.

A static backdrop

Leave closedby off and the backdrop stops dismissing the dialog:

<dialog class="pui-modal" id="terms">
  <!-- ... -->
</dialog>

From JavaScript

The element is a plain <dialog>:

const modal = document.getElementById("confirm");
modal.showModal();
modal.close();
modal.addEventListener("close", () => console.log("closed"));

Moving between two modals

A button carries one command, so opening the next modal leaves the one you are in open behind it, and closing the new one reveals the old. Close it on the same click and you get a swap, which is what a multi-step flow wants:

<dialog class="pui-modal" id="first" closedby="any">
  <div class="pui-card">
    <div class="pui-card-content">
      <button
        class="pui-btn pui-solid pui-theme"
        commandfor="second"
        command="show-modal"
        onclick="this.closest('dialog').close()"
      >
        Continue
      </button>
    </div>
  </div>
</dialog>

Stacking is the browser's own behavior, not something the library adds, so this is the line that turns it into a swap.

If inline handlers are not allowed by your content security policy, the same thing written once for the whole page, which makes swapping the default everywhere:

document.addEventListener("click", (event) => {
  const button = event.target.closest('[command="show-modal"][commandfor]');
  if (button) button.closest("dialog")?.close();
});

Long content

The dialog scrolls on its own once it reaches the height of the viewport.