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
<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
<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
<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.
<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>
| Attribute | Type | Default | Description |
|---|---|---|---|
edge | startendtopbottom | end | Which viewport edge the panel slides from. |
size | smmdlg | md | Panel width (start/end) or height (top/bottom). |
open | boolean | false | Whether 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. |
dismissible | boolean | true | Whether Escape and a backdrop click attempt to close the dialog at all. |
label | string | undefined | – | Accessible 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
| Event | Type | Description |
|---|---|---|
close | Event | Fired 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. |
cancel | Event | Fired 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. |