Riposte

Drawer

A panel that slides in from an edge of the viewport over a dimming scrim — a filter sidebar, a mobile nav, a detail/edit pane. It’s the same modal-overlay machinery…

A panel that slides in from an edge of the viewport over a dimming scrim — a filter sidebar, a mobile nav, a detail/edit pane. It’s the same modal-overlay machinery as Modal (portalled to document.body, animated in and out via usePresence, focus trapped inside via the core useFocusTrap hook, Escape to close), but docked to one edge and sized along that axis. You own open and react to onClose:

val (open, setOpen, _) = useState(false)

Button("Filters", onClick = () => setOpen(true))

Drawer(
  open = open,
  onClose = () => setOpen(false),
  placement = DrawerPlacement.Right,   // Left | Right | Top | Bottom
  size = "360px",                      // width (left/right) or height (top/bottom)
  title = Some(span("Filters")),
  footer = Some(Button("Apply", onClick = () => setOpen(false))),
)(
  filterForm,
)

placement (DrawerPlacement.{Left,Right,Top,Bottom}, default Right) picks the edge; size is a CSS length applied to the docking axis (width for left/right, height for top/bottom). children are the body; title, extra (header-right actions), and footer are optional slots. closable toggles the corner close button; mask shows the scrim and maskClosable lets a scrim click dismiss; closeOnEsc enables Escape; ariaLabel names the dialog when there is no title. exitMs (default 250) is how long the slide-out runs before unmount — keep it in step with the skin’s transition.

It implements the dialog ARIA pattern: role="dialog" and aria-modal, labelled by the title (or ariaLabel), focus moved into the panel on open and restored to the opener on close, Tab trapped inside. Lifecycle state is mirrored to data-state (enter/open/ exit) and data-placement on the root, mask, and panel, so the slide is driven from CSS.

Search

Esc
to navigate to open Esc to close