Navigation

Drawer

<oblyx-drawer> is an edge-anchored panel: the same native <dialog> plus showModal() foundation as dialog, with different CSS: it slides in from a viewport edge instead of appearing centered. Every dialog behaviour on this page (the declarative trigger, dismissal, focus, scroll lock) applies to a drawer identically, because both sit on the same shared base class. Open and close it with a plain <button>'s command/commandfor attributes, no script required, as in the example below. A scripted route also exists, for a consumer who needs it: see "Opening from script" further down this page. Compose it with dialog's own <oblyx-dialog-header>, <oblyx-dialog-body> and <oblyx-dialog-footer>; a drawer has no header-equivalent element of its own.

Basic drawer

Filter controls go here. Apply
HTMLKotlin
<button type="button" command="--show" commandfor="ox-doc-drawer-basic">
  Open filters
</button>
<oblyx-drawer id="ox-doc-drawer-basic" label="Filters">
  <oblyx-dialog-header heading="Filters"></oblyx-dialog-header>
  <oblyx-dialog-body>Filter controls go here.</oblyx-dialog-body>
  <oblyx-dialog-footer>
    <button type="button" command="--close" commandfor="ox-doc-drawer-basic">Cancel</button>
    <oblyx-button variant="primary">Apply</oblyx-button>
  </oblyx-dialog-footer>
</oblyx-drawer>
button {
  attributes["command"] = "--show"
  attributes["commandfor"] = "ox-doc-drawer-basic"
  +"Open filters"
}
oblyxDrawer(label = "Filters") {
  id = "ox-doc-drawer-basic"
  oblyxDialogHeader(heading = "Filters")
  oblyxDialogBody { +"Filter controls go here." }
  oblyxDialogFooter {
    // A plain <button> here, not oblyxButton: this row needs the
    // command/commandfor pair to close the drawer, and oblyx-button has no
    // attribute of its own to carry them.
    button {
      attributes["command"] = "--close"
      attributes["commandfor"] = "ox-doc-drawer-basic"
      +"Cancel"
    }
    oblyxButton(variant = OblyxButtonVariant.PRIMARY) { +"Apply" }
  }
}

The declarative trigger

command="--show" and commandfor="ox-doc-drawer-basic" are the platform's own Invoker Commands API: commandfor names the drawer by id, and command carries the action. <oblyx-drawer> understands the same two command values as dialog, since both are handled by the shared base class: --show opens it, and --close dismisses it through the same cancelable path as Escape and a backdrop click (see "Opening and dismissing" below). The browser wires the click to the drawer entirely on its own. Nothing here is Oblyx-specific markup: the same command/commandfor pair works on a plain native <dialog> too.

This relies on browser support for Invoker Commands, which reached Baseline availability across Chrome, Edge, Firefox and Safari only recently (Chrome/Edge 135+, Firefox 144+, Safari 26.2+). A browser without support treats command/commandfor as ordinary, inert attributes: the button renders and is focusable, it just does not open anything on click, with no error and no visible fallback. A consumer who must support such a browser should use the scripted route instead.

Edges

start/end are logical and mirror under :dir(rtl);top/bottom are the block-axis equivalents. end is the default: the common "panel slides in from the right" case in a left-to-right page.

Drawer edges

Slides in from the start edge (the left, in LTR; mirrors under :dir(rtl)). Slides up from the bottom edge.
HTMLKotlin
<button type="button" command="--show" commandfor="ox-doc-drawer-edge-start">
  From start
</button>
<button type="button" command="--show" commandfor="ox-doc-drawer-edge-bottom">
  From bottom
</button>
<oblyx-drawer id="ox-doc-drawer-edge-start" edge="start" label="Navigation">
  <oblyx-dialog-header heading="Navigation"></oblyx-dialog-header>
  <oblyx-dialog-body>Slides in from the start edge (the left, in LTR; mirrors under :dir(rtl)).</oblyx-dialog-body>
</oblyx-drawer>
<oblyx-drawer id="ox-doc-drawer-edge-bottom" edge="bottom" label="Details">
  <oblyx-dialog-header heading="Details"></oblyx-dialog-header>
  <oblyx-dialog-body>Slides up from the bottom edge.</oblyx-dialog-body>
</oblyx-drawer>
button {
  attributes["command"] = "--show"
  attributes["commandfor"] = "ox-doc-drawer-edge-start"
  +"From start"
}
button {
  attributes["command"] = "--show"
  attributes["commandfor"] = "ox-doc-drawer-edge-bottom"
  +"From bottom"
}
oblyxDrawer(edge = OblyxDrawerEdge.START, label = "Navigation") {
  id = "ox-doc-drawer-edge-start"
  oblyxDialogHeader(heading = "Navigation")
  oblyxDialogBody { +"Slides in from the start edge (the left, in LTR; mirrors under :dir(rtl))." }
}
oblyxDrawer(edge = OblyxDrawerEdge.BOTTOM, label = "Details") {
  id = "ox-doc-drawer-edge-bottom"
  oblyxDialogHeader(heading = "Details")
  oblyxDialogBody { +"Slides up from the bottom edge." }
}

Size

size sets the panel's width on a start/end drawer, or its height on a top/bottom one, from the same OblyxElementSize other components use. Default md.

Drawer size

size="lg" widens the panel (edge start/end) or heightens it (edge top/bottom).
HTMLKotlin
<button type="button" command="--show" commandfor="ox-doc-drawer-size-lg">
  Open large drawer
</button>
<oblyx-drawer id="ox-doc-drawer-size-lg" size="lg" label="Order details">
  <oblyx-dialog-header heading="Order details"></oblyx-dialog-header>
  <oblyx-dialog-body>size="lg" widens the panel (edge start/end) or heightens it (edge top/bottom).</oblyx-dialog-body>
</oblyx-drawer>
button {
  attributes["command"] = "--show"
  attributes["commandfor"] = "ox-doc-drawer-size-lg"
  +"Open large drawer"
}
oblyxDrawer(size = OblyxElementSize.LG, label = "Order details") {
  id = "ox-doc-drawer-size-lg"
  oblyxDialogHeader(heading = "Order details")
  oblyxDialogBody { +"size=\"lg\" widens the panel (edge start/end) or heightens it (edge top/bottom)." }
}

Opening and dismissing

Escape and a backdrop click both close the drawer, gated by dismissible (default true) exactly like dialog: one flag for both, not two, since a drawer that shouldn't lose input to a stray Escape shouldn't lose it to a stray backdrop click either. Escape is intercepted at keydown rather than relying on the native <dialog> cancel event, because that event reports cancelable: false in Chromium and closes the dialog regardless of preventDefault(). A backdrop click is detected by comparing event.target to event.currentTarget: a genuine ::backdrop click always targets the <dialog> element itself, never a child. An <oblyx-dialog-header closable> gets the same treatment as a plain close button, and setting dismissible to false turns off all three at once, the same as dialog. A command="--close" invoker button is subject to the same gate, since it also routes through the cancelable path. dismissible defaults to true, so bare presence (or omitting the attribute) means true; writing the literal string dismissible="false" in server-rendered markup turns it off. See dialog's own note on this for a working example.

Focus

Focus trapping and Tab-cycling come from showModal() making the rest of the page inert while the drawer is open: the platform's own work, not a hand-built trap. On open, focus moves to [data-autofocus] if present, otherwise to the header's title, otherwise to the browser's own default first-focusable-descendant. On close, focus returns to whatever element opened the drawer, whether that was a command="--show" button or a scripted click handler, sequenced strictly after dialog.close() completes.

A dialog and a drawer opened at the same time (a confirm dialog launched from inside a drawer, for instance) share one reference-counted page-scroll lock rather than fighting over it independently: the second opener doesn't stomp the scroll position the first one already saved.

Opening from script

The open property/attribute is the underlying mechanism the declarative trigger itself sets: setting it directly still works, and is the right choice when a consumer needs to support a browser without Invoker Commands, or needs to run other logic (an analytics call, a conditional guard) at the moment of opening. Nothing about the declarative trigger requires this route, and nothing about this route requires the declarative trigger: pick whichever a given drawer needs, independently, drawer by drawer.

Opened from an inline click handler

Equivalent to the basic drawer above, opened with a property assignment instead of command/commandfor.

Filter controls go here. Apply
HTMLKotlin
<button type="button" onclick="document.getElementById('ox-doc-drawer-scripted').open = true">
  Open filters
</button>
<oblyx-drawer id="ox-doc-drawer-scripted" label="Filters">
  <oblyx-dialog-header heading="Filters"></oblyx-dialog-header>
  <oblyx-dialog-body>Filter controls go here.</oblyx-dialog-body>
  <oblyx-dialog-footer>
    <oblyx-button variant="primary">Apply</oblyx-button>
  </oblyx-dialog-footer>
</oblyx-drawer>
button {
  onClick = "document.getElementById('ox-doc-drawer-scripted').open = true"
  +"Open filters"
}
oblyxDrawer(label = "Filters") {
  id = "ox-doc-drawer-scripted"
  oblyxDialogHeader(heading = "Filters")
  oblyxDialogBody { +"Filter controls go here." }
  oblyxDialogFooter {
    oblyxButton(variant = OblyxButtonVariant.PRIMARY) { +"Apply" }
  }
}

Reference

<oblyx-drawer>

AttributeTypeDefaultDescription
edgestartendtopbottomendWhich viewport edge the panel slides from.
sizesmmdlgmdPanel width (start/end) or height (top/bottom).
openbooleanfalseWhether the dialog is open. Setting this directly always takes effect: it is the "force open/closed" path, matching a native <dialog>'s own close(). For a dismissal a user can cancel, see dismiss.
dismissiblebooleantrueWhether Escape and a backdrop click attempt to close the dialog at all.
labelstring | undefinedAccessible name, used when no <oblyx-dialog-header heading> (or <oblyx-drawer>'s own light content) provides one. Required in that case; its absence is flagged loudly, not silently ignored.

Events

EventTypeDescription
closeEventFired once the dialog has finished closing: its exit animation has ended, the underlying dialog element has closed, and focus has returned to whatever opened it.
cancelEventFired when the user attempts to dismiss the dialog through Escape, a backdrop click, or a built-in close control. Cancelable: call preventDefault on this event to keep the dialog open.