Skip to content

Modal

wu-modal styles a native <dialog>. A button with commandfor and command="show-modal" opens it and command="close" closes it, with no JavaScript. Esc closes it too.

Put your content in a <dialog>, then point a button at it. The button opens the dialog, a second button closes it, and the browser handles focus, the dimmed backdrop and the Esc key for you. This uses the browser’s built-in dialog element, with commandfor and command on the buttons instead of a script. The full working demo is under Default.

<button class="wu-btn" type="button" commandfor="my-modal" command="show-modal">Open</button>
<dialog class="wu-modal" id="my-modal" aria-label="Example">
<div class="wu-modal-body">Hello.</div>
<form class="wu-modal-footer" method="dialog">
<button class="wu-btn brand">Close</button>
</form>
</dialog>
Class, attribute or variable Effect
wu-modal Root, on <dialog>. Surface panel with a border, a large radius and a shadow, over a dimmed backdrop
wu-modal-header Part, on <header>. Title row with a line below
wu-modal-title Part. Large semibold title; link it with aria-labelledby
wu-modal-close Part, on a <button> with aria-label. Cross drawn in CSS
wu-modal-body Part. Scrolls on its own when the content is taller than the dialog
wu-modal-footer Part, on <form method="dialog">. End-aligned buttons with a line above
sm, lg 24rem and 48rem wide instead of 32rem
closedby="any" Click outside also closes it
animated Fade and scale in and out, opt-in (see Animation)
--wu-modal--Width Width
--wu-modal--MaxHeight Maximum height
--wu-modal--Backdrop Backdrop color, black at 50% (70% in dark themes) by default
<button class="wu-btn" type="button" commandfor="modal-default" command="show-modal">Edit profile</button>
<dialog class="wu-modal" id="modal-default" aria-labelledby="modal-default-title" closedby="any">
<header class="wu-modal-header">
<h2 class="wu-modal-title" id="modal-default-title">Edit profile</h2>
<button class="wu-modal-close" type="button" commandfor="modal-default" command="close" aria-label="Close"></button>
</header>
<div class="wu-modal-body">
<div class="wu-field">
<label class="wu-field-label" for="modal-default-name">Display name</label>
<input class="wu-input" id="modal-default-name" type="text" value="Ana Lopez" autofocus>
</div>
</div>
<form class="wu-modal-footer" method="dialog">
<button class="wu-btn">Cancel</button>
<button class="wu-btn brand" value="confirm">Save</button>
</form>
</dialog>

The footer is a <form method="dialog">: its buttons close the dialog, and dialog.returnValue holds the value of the button that closed it.

24rem wide.
48rem wide.
<div class="flex wrap gap-2">
<button class="wu-btn" type="button" commandfor="modal-sm" command="show-modal">Small</button>
<button class="wu-btn" type="button" commandfor="modal-lg" command="show-modal">Large</button>
</div>
<dialog class="wu-modal sm" id="modal-sm" aria-labelledby="modal-sm-title" closedby="any">
<header class="wu-modal-header">
<h2 class="wu-modal-title" id="modal-sm-title">Small modal</h2>
<button class="wu-modal-close" type="button" commandfor="modal-sm" command="close" aria-label="Close"></button>
</header>
<div class="wu-modal-body">24rem wide.</div>
</dialog>
<dialog class="wu-modal lg" id="modal-lg" aria-labelledby="modal-lg-title" closedby="any">
<header class="wu-modal-header">
<h2 class="wu-modal-title" id="modal-lg-title">Large modal</h2>
<button class="wu-modal-close" type="button" commandfor="modal-lg" command="close" aria-label="Close"></button>
</header>
<div class="wu-modal-body">48rem wide.</div>
</dialog>

Any other width: style="--wu-modal--Width: 40rem". The modal never grows past the viewport width minus a margin.

The header and footer stay in place; the body scrolls.

These terms apply to every workspace you create or join.

You keep the rights to the content you upload. We store it only to run the service.

Either side can end the agreement at any time. Your data stays available for export for 30 days.

We announce changes to these terms by email before they apply.

Questions go to the support address listed in your account settings.

<button class="wu-btn" type="button" commandfor="modal-long" command="show-modal">Read the terms</button>
<dialog class="wu-modal" id="modal-long" aria-labelledby="modal-long-title" closedby="any" style="--wu-modal--MaxHeight: 22rem">
<header class="wu-modal-header">
<h2 class="wu-modal-title" id="modal-long-title">Terms of service</h2>
<button class="wu-modal-close" type="button" commandfor="modal-long" command="close" aria-label="Close"></button>
</header>
<div class="wu-modal-body">
<p>These terms apply to every workspace you create or join.</p>
<p>You keep the rights to the content you upload. We store it only to run the service.</p>
<p>Either side can end the agreement at any time. Your data stays available for export for 30 days.</p>
<p>We announce changes to these terms by email before they apply.</p>
<p>Questions go to the support address listed in your account settings.</p>
</div>
<form class="wu-modal-footer" method="dialog">
<button class="wu-btn brand" value="accept">Accept</button>
</form>
</dialog>

The animated word fades and scales the dialog in and out and fades the backdrop. wu-animated on an ancestor turns it on for every modal inside. Motion runs only under prefers-reduced-motion: no-preference.

Invitations expire after seven days.
<button class="wu-btn" type="button" commandfor="modal-animated" command="show-modal">Open animated</button>
<dialog class="wu-modal animated" id="modal-animated" aria-labelledby="modal-animated-title" closedby="any">
<header class="wu-modal-header">
<h2 class="wu-modal-title" id="modal-animated-title">Invite members</h2>
<button class="wu-modal-close" type="button" commandfor="modal-animated" command="close" aria-label="Close"></button>
</header>
<div class="wu-modal-body">Invitations expire after seven days.</div>
<form class="wu-modal-footer" method="dialog">
<button class="wu-btn">Cancel</button>
<button class="wu-btn brand" value="send">Send invites</button>
</form>
</dialog>

While a modal is open with showModal() (or command="show-modal"), the page behind it does not scroll. The rule is CSS only: :root:has(.wu-modal:modal) sets overflow: hidden on the page, and scrollbar-gutter: stable keeps the space of the page scrollbar, so the layout does not shift. A dialog opened with show() is not modal and leaves the page scrolling. The body of a long modal still scrolls on its own.

The close button sits at the inline end of the header and the footer buttons line up at the inline end, so both move to the left in right-to-left text.

يسري الاسم الجديد على كل رابط مشترك.
<div dir="rtl">
<button class="wu-btn" type="button" commandfor="modal-rtl" command="show-modal">فتح</button>
<dialog class="wu-modal" id="modal-rtl" aria-labelledby="modal-rtl-title" closedby="any">
<header class="wu-modal-header">
<h2 class="wu-modal-title" id="modal-rtl-title">إعادة تسمية الملف</h2>
<button class="wu-modal-close" type="button" commandfor="modal-rtl" command="close" aria-label="إغلاق"></button>
</header>
<div class="wu-modal-body">يسري الاسم الجديد على كل رابط مشترك.</div>
<form class="wu-modal-footer" method="dialog">
<button class="wu-btn">إلغاء</button>
<button class="wu-btn brand" value="confirm">إعادة تسمية</button>
</form>
</dialog>
</div>
  • showModal() (here through command="show-modal") makes the rest of the page inert, moves focus into the dialog and returns it to the trigger on close.
  • Put autofocus on the first field of a form dialog, as in the default example, so typing can start at once. Without it, focus goes to the first focusable element, usually the close button.
  • Link the title with aria-labelledby and give the close button an aria-label.
  • Esc closes the dialog; closedby="any" adds click outside.
  • Keyboard focus on the close button shows the focus ring.
@use "@wovenui/components/modal";

The modal needs no other component. The examples above also use the button, the field and the input.