Skip to content

Drawer

A drawer is a native <dialog> with .re-drawer added alongside .re-dialog. It opens with the same dialog wiring — enhanceDialog and the data-re-dialog-trigger / -target / -close / -close-on-backdrop attributes — so the browser owns the top layer, backdrop, focus trap, Escape, and inert background. This file only changes the box geometry and adds a slide-in.

import { enhanceDialog } from "@relements/core/behaviors/dialog";
enhanceDialog(document);

Set the edge with data-side: end (default), start, top, bottom — pinned with logical insets, so start/end follow the writing direction. Size along the pin axis with data-size="sm | md | lg". Give every drawer an accessible name (aria-labelledby → its .re-dialog__title, or aria-label).

The slide uses @starting-style + allow-discrete and degrades to an instant show/hide on engines without them (and under prefers-reduced-motion). Dismissal is Escape or backdrop only — there’s no swipe gesture.

Settings

Drawer pinned to the inline-end edge. Scrolls independently when tall.

Navigation

Drawer pinned to the inline-start edge.

Notifications

Sheet pinned to the block-start edge.

Filters

Bottom sheet. Dismissal is Escape or backdrop — no swipe gesture.

Live example
<div class="drawer-triggers">
  <button
    type="button"
    class="re-button"
    data-re-dialog-trigger
    data-re-dialog-target="drawer-end"
  >
    End
  </button>
  <button
    type="button"
    class="re-button"
    data-variant="secondary"
    data-re-dialog-trigger
    data-re-dialog-target="drawer-start"
  >
    Start
  </button>
  <button
    type="button"
    class="re-button"
    data-variant="secondary"
    data-re-dialog-trigger
    data-re-dialog-target="drawer-top"
  >
    Top
  </button>
  <button
    type="button"
    class="re-button"
    data-variant="secondary"
    data-re-dialog-trigger
    data-re-dialog-target="drawer-bottom"
  >
    Bottom
  </button>
</div>

<dialog
  class="re-dialog re-drawer"
  id="drawer-end"
  data-side="end"
  aria-labelledby="drawer-end-title"
  data-re-dialog-close-on-backdrop
>
  <header class="re-dialog__header">
    <h2 class="re-dialog__title" id="drawer-end-title">Settings</h2>
    <button class="re-dialog__close" aria-label="Close" data-re-dialog-close value="close">
      &times;
    </button>
  </header>
  <div class="re-dialog__body">
    <p>Drawer pinned to the inline-end edge. Scrolls independently when tall.</p>
  </div>
  <footer class="re-dialog__footer">
    <button
      type="button"
      class="re-button"
      data-variant="ghost"
      data-re-dialog-close
      value="cancel"
    >
      Cancel
    </button>
    <button type="button" class="re-button" data-re-dialog-close value="save">Save</button>
  </footer>
</dialog>

<dialog
  class="re-dialog re-drawer"
  id="drawer-start"
  data-side="start"
  aria-labelledby="drawer-start-title"
  data-re-dialog-close-on-backdrop
>
  <header class="re-dialog__header">
    <h2 class="re-dialog__title" id="drawer-start-title">Navigation</h2>
    <button class="re-dialog__close" aria-label="Close" data-re-dialog-close value="close">
      &times;
    </button>
  </header>
  <div class="re-dialog__body"><p>Drawer pinned to the inline-start edge.</p></div>
</dialog>

<dialog
  class="re-dialog re-drawer"
  id="drawer-top"
  data-side="top"
  aria-labelledby="drawer-top-title"
  data-re-dialog-close-on-backdrop
>
  <header class="re-dialog__header">
    <h2 class="re-dialog__title" id="drawer-top-title">Notifications</h2>
    <button class="re-dialog__close" aria-label="Close" data-re-dialog-close value="close">
      &times;
    </button>
  </header>
  <div class="re-dialog__body"><p>Sheet pinned to the block-start edge.</p></div>
</dialog>

<dialog
  class="re-dialog re-drawer"
  id="drawer-bottom"
  data-side="bottom"
  aria-labelledby="drawer-bottom-title"
  data-re-dialog-close-on-backdrop
>
  <header class="re-dialog__header">
    <h2 class="re-dialog__title" id="drawer-bottom-title">Filters</h2>
    <button class="re-dialog__close" aria-label="Close" data-re-dialog-close value="close">
      &times;
    </button>
  </header>
  <div class="re-dialog__body">
    <p>Bottom sheet. Dismissal is Escape or backdrop — no swipe gesture.</p>
  </div>
</dialog>

A drawer is a native modal <dialog> opened with showModal(), so its accessibility comes straight from the platform — the same as dialog.

  • Keyboard — opens from the trigger button (Enter/Space). The browser traps focus inside the open panel: Tab and Shift+Tab cycle through its controls without leaving. Escape dismisses it (along with a backdrop click when data-re-dialog-close-on-backdrop is set). There is no swipe gesture.
  • Focus — on open, focus moves into the panel and the background goes inert; on close it returns to the trigger — all native <dialog> behavior. Interactive controls show the standard :focus-visible ring (the close button uses --re-shadow-focus).
  • Semantics — the <dialog> is announced as a modal dialog. Give every drawer an accessible name: aria-labelledby pointing at its .re-dialog__title, or aria-label. The icon close button carries aria-label="Close". data-side and data-size are presentation only and add no semantics.
  • Notes — the slide-in and backdrop fade collapse to an instant show/hide under prefers-reduced-motion and on engines without @starting-style / allow-discrete. See the accessibility guide.