Drawer
Category: Overlays · Tags: drawer, off-canvas, side-panel, dialog, filters
How to use it
Use for secondary content that should stay one step away from the page, such as filters, a cart summary or settings on a small screen. Do not use it for a short yes/no question (use a confirmation dialog) or for primary navigation that should always be visible. After copying, rename the id dcl-drawer-panel and its aria-controls/aria-labelledby references, and replace the filter fields with your own.
Accessibility
Built on the native <dialog> element opened with showModal(), so the page behind is inert, Tab stays inside the panel, and Escape closes it without extra code; aria-labelledby names it from its heading. Closing it any way (Close, Apply filters, Escape, or a click outside the panel) returns focus to the button that opened it, and every control has a visible 2px focus outline. Panel text is #0f172a on #ffffff (17.85:1), the legend is #334155 on #ffffff (10.35:1), and the white-on-#1d4ed8 buttons are 6.70:1 (8.72:1 on hover).
Source (copy-ready)
<div class="dcl-drawer">
<button type="button" class="dcl-drawer__open" aria-haspopup="dialog" aria-controls="dcl-drawer-panel">Filters</button>
<dialog class="dcl-drawer__panel" id="dcl-drawer-panel" aria-labelledby="dcl-drawer-title">
<div class="dcl-drawer__header">
<h2 class="dcl-drawer__title" id="dcl-drawer-title">Filter orders</h2>
<button type="button" class="dcl-drawer__close">Close</button>
</div>
<form class="dcl-drawer__form" method="dialog">
<fieldset class="dcl-drawer__group">
<legend class="dcl-drawer__legend">Order status</legend>
<label class="dcl-drawer__option"><input type="checkbox" name="status" value="open" checked> Open</label>
<label class="dcl-drawer__option"><input type="checkbox" name="status" value="shipped"> Shipped</label>
<label class="dcl-drawer__option"><input type="checkbox" name="status" value="returned"> Returned</label>
</fieldset>
<button type="submit" class="dcl-drawer__apply">Apply filters</button>
</form>
</dialog>
</div>
<style>
.dcl-drawer { font: inherit; color: #0f172a; background-color: #ffffff; }
.dcl-drawer__open, .dcl-drawer__apply { font: inherit; font-weight: 600; padding: 0.55rem 1.1rem; border: 1px solid #1d4ed8; border-radius: 6px; cursor: pointer; color: #ffffff; background-color: #1d4ed8; }
.dcl-drawer__open:hover, .dcl-drawer__apply:hover { color: #ffffff; background-color: #1e40af; }
.dcl-drawer__close { font: inherit; padding: 0.4rem 0.8rem; border: 1px solid #94a3b8; border-radius: 6px; cursor: pointer; color: #0f172a; background-color: #ffffff; }
.dcl-drawer__close:hover { color: #0f172a; background-color: #f1f5f9; }
.dcl-drawer__open:focus-visible, .dcl-drawer__apply:focus-visible, .dcl-drawer__close:focus-visible { outline: 2px solid #0f172a; outline-offset: 2px; }
.dcl-drawer__panel { margin: 0 0 0 auto; padding: 0; border: none; border-left: 1px solid #cbd5e1; width: 20rem; max-width: 90vw; height: 100%; max-height: 100%; box-sizing: border-box; color: #0f172a; background-color: #ffffff; animation: dcl-drawer-in 200ms ease-out; }
.dcl-drawer__header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 1rem; border-bottom: 1px solid #e2e8f0; }
.dcl-drawer__title { margin: 0; font-size: 1.15rem; color: #0f172a; background-color: #ffffff; }
.dcl-drawer__form { padding: 1rem; }
.dcl-drawer__group { margin: 0 0 1rem; padding: 0; border: none; min-width: 0; }
.dcl-drawer__legend { padding: 0; margin-bottom: 0.5rem; font-weight: 600; color: #334155; background-color: #ffffff; }
.dcl-drawer__option { display: flex; align-items: center; gap: 0.5rem; padding: 0.35rem 0; color: #0f172a; background-color: #ffffff; }
.dcl-drawer__option input { width: 1.1rem; height: 1.1rem; accent-color: #1d4ed8; }
@keyframes dcl-drawer-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
@media (prefers-reduced-motion: reduce) { .dcl-drawer__panel { animation: none; } }
</style>
<script>
(function () {
document.querySelectorAll(".dcl-drawer").forEach(function (root) {
if (root.dataset.dclBound) { return; }
root.dataset.dclBound = "true";
var opener = root.querySelector(".dcl-drawer__open");
var panel = root.querySelector(".dcl-drawer__panel");
var closeButton = root.querySelector(".dcl-drawer__close");
if (!opener || !panel || typeof panel.showModal !== "function") { return; }
opener.addEventListener("click", function () {
if (!panel.open) { panel.showModal(); }
});
if (closeButton) {
closeButton.addEventListener("click", function () { panel.close(); });
}
// A click on the dimmed area outside the panel closes it.
panel.addEventListener("click", function (event) {
var box = panel.getBoundingClientRect();
var outside = event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom;
if (outside && event.target === panel) { panel.close(); }
});
// Escape, Close, Apply and outside clicks all end here.
panel.addEventListener("close", function () { opener.focus(); });
});
})();
</script>
Was this useful? Send feedback.