ComponentsModal

A dialog element styled as a modal, with open and close transitions from plain showModal() and close().

On this page

A modal is a <dialog> with an <article> inside. The dialog fills the viewport as an overlay with a blurred backdrop, and the article is the box. Open it with showModal() and close it with close(). The browser gives you the top layer, focus handling and the Escape key.

Modal
<button type="button" id="open">Open the modal</button>

<dialog id="modal">
  <article>
    <header>
      <button type="button" aria-label="Close" rel="prev" id="close"></button>
      <p><strong>Delete this project?</strong></p>
    </header>
    <p>This cannot be undone.</p>
    <footer>
      <button type="button" class="secondary" id="cancel">Cancel</button>
      <button type="button" id="confirm">Delete</button>
    </footer>
  </article>
</dialog>

<script>
  const modal = document.getElementById("modal");
  document.getElementById("open").addEventListener("click", () => modal.showModal());
  for (const id of ["close", "cancel", "confirm"]) {
    document.getElementById(id).addEventListener("click", () => modal.close());
  }
</script>

Structure

Header, body and footer are the same as in a card. In the header, a button or link with rel="prev" (or the class close) becomes a close icon floated to the end. Buttons in the footer are aligned to the end and spaced apart. The article is capped at 510px from 576px wide and at 700px from 768px wide, and it scrolls inside the dialog if it is taller than the screen.

A dialog that is not open is display: none. A dialog with the open attribute written in HTML shows right away, which is how the test fixtures render it.

Transitions

With plain showModal() and close() the overlay fades in and the article scales up from 96%, then both reverse on close. This is done with @starting-style and transition-behavior: allow-discrete, so there are no classes and no timers.

The transition is skipped for people who prefer reduced motion, and in browsers without transition-behavior, where the modal still opens and closes, only without animation.

The legacy class flow

Pico CSS v2.1.1 animated modals with three classes on <html>, set from JavaScript: modal-is-open, modal-is-opening and modal-is-closing. Pico Reforged keeps them, so pages ported from v2.1.1 keep working. While modal-is-open is on the root, the page behind is not scrollable and ignores pointer events, and the plain transitions above are switched off so only one animation runs.

Class based flow
<button type="button" id="open">Open the modal</button>

<dialog id="modal">
  <article>
    <header>
      <button type="button" aria-label="Close" rel="prev" id="close"></button>
      <p><strong>Confirm your order</strong></p>
    </header>
    <p>Your cart has three items and ships on Friday.</p>
  </article>
</dialog>

<script>
  const root = document.documentElement;
  const modal = document.getElementById("modal");

  function openModal() {
    const gap = window.innerWidth - root.clientWidth;
    root.style.setProperty("--pico-scrollbar-width", gap + "px");
    root.classList.add("modal-is-open", "modal-is-opening");
    modal.setAttribute("open", "");
    setTimeout(() => root.classList.remove("modal-is-opening"), 400);
  }

  function closeModal() {
    root.classList.add("modal-is-closing");
    setTimeout(() => {
      root.classList.remove("modal-is-open", "modal-is-closing");
      root.style.removeProperty("--pico-scrollbar-width");
      modal.removeAttribute("open");
    }, 400);
  }

  document.getElementById("open").addEventListener("click", openModal);
  document.getElementById("close").addEventListener("click", closeModal);
</script>

The script sets --pico-scrollbar-width so the page does not jump when the scrollbar goes away. Use this flow only if you need the exact v2.1.1 behavior. A dialog opened with the open attribute is not modal to the browser, so it has no top layer, no focus trap and no Escape key. Prefer showModal() for new work.

The modal-is-* classes are also part of the class-less build and of the scoped conditional build, where they stay global because they sit on <html>.

Accessibility

showModal() makes the rest of the page inert, moves focus into the dialog and returns it to the opener on close. Give the dialog a name with aria-labelledby pointing at the title, and give an icon-only close button an aria-label. If you use the class flow with the open attribute, handle focus and Escape yourself.

Customization

--pico-modal-overlay-background-color
light-dark(oklch(0.2 0.02 265 / 0.4), oklch(0 0 0 / 0.6))
Overlay color behind the box.
--pico-modal-overlay-backdrop-filter
blur(0.375rem)
Blur applied to the page behind the overlay.
--pico-scrollbar-width
0
Padding added to the root while a modal is open. Set it from script, as above.
--pico-icon-close
SVG data URI
The close icon image, an SVG data URI.
--pico-card-border-radius
calc(var(--pico-border-radius) * 1.5)
The modal box uses 4/3 of the card radius.