Skip to content

Drawer

wu-drawer styles a native <dialog> as a panel docked to an edge of the viewport. It opens and closes like the modal: commandfor with command="show-modal" and command="close", and Esc.

How to make a slide-out drawer without JavaScript

Section titled “How to make a slide-out drawer without JavaScript”

A drawer is a dialog that sticks to the side of the screen. Give a button the id of the drawer and it opens, with no script to write. This uses the browser’s built-in dialog element, with commandfor and command on the buttons. Add animated if you want it to slide in. The full demo is under Inline end.

<button class="wu-btn" type="button" commandfor="my-drawer" command="show-modal">Filters</button>
<dialog class="wu-drawer" id="my-drawer" aria-label="Filters">
<div class="wu-drawer-body">Options go here.</div>
<form class="wu-drawer-footer" method="dialog">
<button class="wu-btn brand">Done</button>
</form>
</dialog>
Class, attribute or variable Effect
wu-drawer Root, on <dialog>. Full-height surface panel at the inline end, over a dimmed backdrop
wu-drawer-header Part, on <header>. Title row with a line below
wu-drawer-title Part. Large semibold title; link it with aria-labelledby
wu-drawer-close Part, on a <button> with aria-label. Cross drawn in CSS
wu-drawer-body Part. Scrolls on its own
wu-drawer-footer Part, on <form method="dialog">. End-aligned buttons with a line above
inline-start Docks to the inline start instead
bottom Docks to the bottom, full width, with rounded top corners and room for the device’s bottom safe area
closedby="any" Click outside also closes it
animated Slides in from its edge and back out, opt-in
--wu-drawer--Width Width of a side drawer, 24rem by default
--wu-drawer--MaxHeight Maximum height of a bottom drawer, 75dvh by default
--wu-drawer--Backdrop Backdrop color, black at 50% (70% in dark themes) by default

The side modifiers are inline-start and bottom. The words start and end are core class names (flex alignment), so the kit does not use them as modifiers.

Filters

<button class="wu-btn" type="button" commandfor="drawer-end" command="show-modal">Filters</button>
<dialog class="wu-drawer" id="drawer-end" aria-labelledby="drawer-end-title" closedby="any">
<header class="wu-drawer-header">
<h2 class="wu-drawer-title" id="drawer-end-title">Filters</h2>
<button class="wu-drawer-close" type="button" commandfor="drawer-end" command="close" aria-label="Close"></button>
</header>
<div class="wu-drawer-body">
<div class="wu-field">
<label class="wu-field-label" for="drawer-end-status">Status</label>
<select class="wu-select" id="drawer-end-status" form="drawer-end-form" autofocus>
<option>Any</option>
<option>Open</option>
<option>Closed</option>
</select>
</div>
</div>
<form class="wu-drawer-footer" id="drawer-end-form" method="dialog">
<button class="wu-btn" type="reset">Reset</button>
<button class="wu-btn brand" value="apply">Apply</button>
</form>
</dialog>

form="drawer-end-form" ties the select to the footer form, so type="reset" puts it back to its first value and leaves the drawer open, while Apply closes it.

Menu

Docked to the inline start: the left in left-to-right pages, the right in right-to-left pages.

Share

Docked to the bottom, full width.
<div class="flex wrap gap-2">
<button class="wu-btn" type="button" commandfor="drawer-start" command="show-modal">Inline start</button>
<button class="wu-btn" type="button" commandfor="drawer-bottom" command="show-modal">Bottom</button>
</div>
<dialog class="wu-drawer inline-start" id="drawer-start" aria-labelledby="drawer-start-title" closedby="any">
<header class="wu-drawer-header">
<h2 class="wu-drawer-title" id="drawer-start-title">Menu</h2>
<button class="wu-drawer-close" type="button" commandfor="drawer-start" command="close" aria-label="Close"></button>
</header>
<div class="wu-drawer-body">Docked to the inline start: the left in left-to-right pages, the right in right-to-left pages.</div>
</dialog>
<dialog class="wu-drawer bottom" id="drawer-bottom" aria-labelledby="drawer-bottom-title" closedby="any">
<header class="wu-drawer-header">
<h2 class="wu-drawer-title" id="drawer-bottom-title">Share</h2>
<button class="wu-drawer-close" type="button" commandfor="drawer-bottom" command="close" aria-label="Close"></button>
</header>
<div class="wu-drawer-body">Docked to the bottom, full width.</div>
</dialog>

The animated word slides the drawer in from its edge and back out, mirrored in right-to-left pages, and fades the backdrop. wu-animated on an ancestor turns it on for every drawer inside. Motion runs only under prefers-reduced-motion: no-preference.

Activity

No new activity.
<button class="wu-btn" type="button" commandfor="drawer-animated" command="show-modal">Open animated</button>
<dialog class="wu-drawer animated" id="drawer-animated" aria-labelledby="drawer-animated-title" closedby="any">
<header class="wu-drawer-header">
<h2 class="wu-drawer-title" id="drawer-animated-title">Activity</h2>
<button class="wu-drawer-close" type="button" commandfor="drawer-animated" command="close" aria-label="Close"></button>
</header>
<div class="wu-drawer-body">No new activity.</div>
</dialog>

While a drawer is open with command="show-modal" (or showModal()), the page behind it does not scroll. The rule is CSS only: :root:has(.wu-drawer: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 drawer opened with show() is not modal and leaves the page scrolling. The drawer body still scrolls on its own.

The default drawer docks to the inline end, which is the left edge in right-to-left text, and inline-start docks to the right. The close button and the footer buttons move to the left too, and the animated slide comes from the drawer’s own edge.

التفاصيل

يلتصق بنهاية السطر: الحافة اليسرى في النص من اليمين إلى اليسار.
<div dir="rtl">
<button class="wu-btn" type="button" commandfor="drawer-rtl" command="show-modal">فتح</button>
<dialog class="wu-drawer animated" id="drawer-rtl" aria-labelledby="drawer-rtl-title" closedby="any">
<header class="wu-drawer-header">
<h2 class="wu-drawer-title" id="drawer-rtl-title">التفاصيل</h2>
<button class="wu-drawer-close" type="button" commandfor="drawer-rtl" command="close" aria-label="إغلاق"></button>
</header>
<div class="wu-drawer-body">يلتصق بنهاية السطر: الحافة اليسرى في النص من اليمين إلى اليسار.</div>
<form class="wu-drawer-footer" method="dialog">
<button class="wu-btn brand" value="done">تم</button>
</form>
</dialog>
</div>
  • command="show-modal" opens the drawer as a modal dialog: the rest of the page is inert and focus moves into the drawer.
  • Put autofocus on the first field of a form drawer, as in the first example. Without it, focus goes to the first focusable element, usually the close button.
  • The bottom drawer pads its end by env(safe-area-inset-bottom), which is above zero only on pages with viewport-fit=cover in the viewport meta tag.
  • Link the title with aria-labelledby and give the close button an aria-label.
  • Esc closes the drawer; closedby="any" adds click outside.
@use "@wovenui/components/drawer";

The drawer needs no other component. The examples above also use the button, the field and the select.