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.
How to make a modal without JavaScript
Section titled “How to make a modal without JavaScript”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 |
Default
Section titled “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.
<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.
Long content
Section titled “Long content”The header and footer stay in place; the body scrolls.
<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>Animation
Section titled “Animation”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.
<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>Page scroll lock
Section titled “Page scroll lock”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.
Right to left
Section titled “Right to left”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>Accessibility
Section titled “Accessibility”showModal()(here throughcommand="show-modal") makes the rest of the page inert, moves focus into the dialog and returns it to the trigger on close.- Put
autofocuson 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-labelledbyand give the close button anaria-label. - Esc closes the dialog;
closedby="any"adds click outside. - Keyboard focus on the close button shows the focus ring.
Load only the modal
Section titled “Load only the modal”@use "@wovenui/components/modal";The modal needs no other component. The examples above also use the button, the field and the input.