Drawer
A panel that slides in from the screen edge, often used for filters, details, and form editing.
Four directions
placement supports left / right / top / bottom. Horizontal placements manage width; vertical placements manage height — width / size controls the panel height for top / bottom.
Four placements
Slides from the top; `size="small"` sets the height to 256px here.
Slides from the bottom — the mainstream form on mobile action sheets.
Status: All
Category: All
Sort: Created time
Right is the default direction; click the mask, the close button, or press Esc to close.
Vertical height (height)
height directly sets the height of top/bottom drawers (overriding the vertical mapping of width / size; silently ignored by left/right drawers). Plain numbers are treated as px; lengths and percentages pass through.
Vertical height (height)
height="40vh": 40% of the viewport height — taller than the default 320px.
height="280": a plain number is treated as pixels.
Animation and lifecycle events
Open/close transitions are animated (transform/opacity) and fall back to instant switching under prefers-reduced-motion. Lifecycle events: oas-open / oas-after-open / oas-close (detail carries the close source) / oas-after-close.
Lifecycle events
Operate the footer buttons / mask / Esc and watch the messages (`oas-close` carries the close source).
Cancel-semantics event (oas-cancel)
Cancel-semantic closes (cancel button / mask click / Esc) dispatch oas-cancel (detail.source tells the origin), distinct from confirm closing (oas-ok); ✕ / swipe / programmatic closes do not dispatch it. Use it to clean up temporary state or track cancellations.
Cancel-semantics event
Close via "Cancel" / mask / Esc → a message shows oas-cancel; click "Save" → oas-ok; ✕ shows neither.
Close interception (before-close)
Listen to the cancelable oas-before-close (event.detail.source tells the close origin) and call preventDefault() to block closing — for unsaved-data protection.
Close interception
Clicking "Discard" / ✕ / mask / Esc is all intercepted: "You have unsaved changes" and the drawer stays open. "Save" closes normally.
Custom footer / header extensions
A footer slot with content hides the built-in OK/Cancel buttons; a header-actions slot renders extra actions right of the title; ok-text / cancel-text override button labels.
Slots and labels
The tag right of the title comes from the `header-actions` slot.
Loading and async OK state
loading shows skeleton placeholders in the content area and disables the buttons (async detail loading); ok-loading puts the OK button into a loading state (spinner + no double trigger) for async submits.
loading / async OK
Content loads asynchronously; this section is covered by skeletons.
Clicking "Submit" puts the OK button into loading; it closes automatically 2s later with a success message.
Resizable
resizable shows a drag rail on the free edge; drag it or use arrow keys to change width/height (clamped by resize-min / resize-max). oas-resize fires on release.
Resizable
Drag the vertical rail on the left edge of the panel (or use arrow keys); the new width is reported after release.
Mobile gestures: swipe to close + snap points
swipeable enables drag-to-close (start from the handle / header; close on threshold or a fast flick). A bottom drawer with snap-points (viewport fractions or pixels) snaps to the nearest point on release.
Gesture drawer
Drag the top handle: on release it snaps to 40% / 85% of the viewport height; drag past the threshold or flick to close.
Drag the handle downward to close.
Imperative API
drawer(options) opens a drawer without templates and returns a { close() } handle; when onOk returns a Promise, the OK button loading is managed automatically.
Imperative
Nested drawers
Stacked drawers get automatic stack management: later openers sit on top (incremental z-index), Esc closes them layer by layer, and the focus trap only applies to the topmost drawer.
Nested layers
Open an inner drawer from within the outer one and observe the stacking.
The inner drawer covers the outer one; press Esc to close the inner first, then again for the outer.
Render and mount strategies
destroy-on-close clears the content after the close animation (re-initialize on next open); append-to mounts the panel into a target container (escapes overflow clipping).
destroy-on-close / append-to
Content is cleared after closing; fill it again before the next open (see the button below).
The panel is mounted into a body-level container, so it is not clipped even inside an `overflow: hidden` host.
Header / close button / Esc switches
no-header hides the whole header, no-close-btn hides ✕, no-esc-close disables Esc closing — giving the host full control over close entries.
Close entries
No header, no ✕, no Esc — close via the button below or the mask (mask click still works).
Initial focus
initial-focus specifies which element receives focus on open (a CSS selector; the panel is searched first, then light DOM); falls back to ✕ when nothing matches.
Initial focus
Controlled visibility
visible is a controlled attribute: the host (button / JS) sets or removes it; remove visible after listening to events to close.
Controlled visibility (visible)
External buttons set / remove `visible` to control visibility without relying on the footer buttons.
Controlled / uncontrolled contract: the component is uncontrolled + event-notification. On every user close path (mask / ✕ / Esc / cancel) it removes its own visible attribute and emits oas-close (the source is in detail), so the host visible attribute always reflects the real state and never goes stale. For declarative binding (host state drives visible), sync the state to closed inside an oas-close handler so it is not written back after closing; intercepting oas-before-close with preventDefault() keeps the drawer open with visible untouched.
No footer / no mask close
no-footer / no-mask-close
The footer action area is hidden; only ✕ and Esc remain as close entries.
Clicking the mask won't close it; use ✕ / Esc or the footer buttons.
No mask mode (no-mask)
no-mask renders no mask layer (the page stays visible and unblocked) while clicking anywhere outside the drawer still closes it (reusing the mask click mask cancel semantics, emitting oas-close / oas-cancel). Suited for lightweight scenarios that keep the page visible, such as side tool panels or persistent filter rails; combined with no-mask-close, outside clicks no longer close (only ✕ / Esc / buttons remain). Non-modal semantics follow dialog.show(): aria-modal="false", no focus steal on open, no Tab trapping, no focus restore on close; focus semantics are captured at open time (toggling no-mask at runtime updates aria and the focus trap but does not change the focus-restore behavior of an already-open drawer).
No mask mode
The page is not dimmed; clicking anywhere outside the drawer closes it (Esc and ✕ still work).
Size presets
Size presets
small tier: 256px, suited for auxiliary info on narrow screens.
large tier: 736px, suited for complex forms or detail scenarios.
Focus trap / scroll lock / z-index switches
no-focus-trap turns off the Tab focus trap (for drawers embedded in a host dialog where focus is managed by the outer layer); no-scroll-lock turns off the body scroll lock (the page keeps scrolling); z-index explicitly sets the panel tier (defaults to the overlay tier + stack-depth offset).
no-focus-trap / no-scroll-lock / z-index
This drawer does not trap Tab focus or lock page scrolling; the tier is explicitly set to z-index="10"; ✕ / Esc / mask close as usual.
Event binding (onoas-*)
Drawer events can be bound directly in the template via onoas-* attributes (onoas-ok / onoas-close / onoas-open …), equivalent to an @oas-ok listener in the host framework.
Event feedback
Click OK or Cancel / ✕ and watch the messages in the top-right corner.
API
oas-drawer
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
append-to | Portal mount point: moves the mask and panel into an isolated shadow inside the target container (body or CSS selector, styles stay scoped), escaping host overflow clipping; moved back to the host shadow when the attribute is removed or unmatched | — | — |
cancel-text | Cancel button text; falls back to locale drawer.cancel | — | — |
destroy-on-close | Clear the host children after the close animation finishes (re-rendered on next open) | boolean | — |
height | Explicit height for vertical drawers (placement top/bottom), overriding width/size; ignored by left/right | — | — |
initial-focus | Focus the element matching this selector on open (panel first, then host light DOM); falls back to the ✕ button / first focusable | — | — |
loading | Body loading state: content hidden with a skeleton placeholder; OK/Cancel buttons disabled too | boolean | — |
no-close-btn | Hide the ✕ close button in the title bar | boolean | — |
no-esc-close | Disable closing with Esc | boolean | — |
no-focus-trap | Disable the focus trap (Tab no longer cycles inside the drawer) | boolean | — |
no-footer | Hide footer action buttons | boolean | — |
no-header | Hide the whole header (also removes the aria-labelledby association) | boolean | — |
no-mask | No-mask mode: no mask rendered; clicking outside still closes with mask cancel semantics | boolean | — |
no-mask-close | Disable closing on mask click | boolean | — |
no-scroll-lock | Do not lock body scroll while open | boolean | — |
ok-loading | OK button loading state: spinner + disabled (aria-busy), blocking repeated triggers | boolean | — |
ok-text | OK button text; falls back to locale drawer.ok | — | — |
placement | Slide direction | string | right |
resizable | Show the edge resize rail: drag to resize along the main axis (width/height per placement), arrow keys step ±8px, Home/End jump to min/max | boolean | — |
resize-max | Resize upper bound (px, default 1000) | string | 1000 |
resize-min | Resize lower bound (px, default 160) | string | 160 |
size | Preset size or a concrete value: small (256px) / medium (378px) / large (736px), or write directly like 512px, 40% | — | — |
snap-points | Snap points array (comma-separated; values ≤1 are viewport-height fractions, >1 are pixels, e.g. 0.35,0.6): bottom/top drawers snap to the nearest point on release and start at the highest point; bottom + snap-points implies gestures | — | — |
swipeable | Enable gestures: drag from the handle/title bar to close (past 35% of the panel size or a fast fling); implied by snap-points (bottom/top) | boolean | — |
title | Title text (rendered into the visible title region; absorbed from the host on read so no native hover tooltip remains; pass an empty string to clear); use the "title" slot for rich content | string | — |
visible | Whether shown | boolean | — |
width | Drawer width (px or percentage), takes precedence over size | — | — |
z-index | Explicit z-index base: stacked on --oas-z-index-base (the panel sits 1 above the mask); defaults to the overlay tier --oas-z-overlay, incrementing with stack depth when nested | — | — |
Events
| Event | Description |
|---|---|
oas-after-close | Close animation finished (canonical, aligned with the after-* family); no detail. Scroll unlock and focus restore happen after this |
oas-after-open | Open animation finished (canonical, aligned with the after-* family); no detail; same as oas-opened |
oas-before-close | Emitted before a close request (✕ / cancel / mask / Esc / OK / swipe), cancelable, detail: { source }; preventDefault blocks the close |
oas-cancel | Dispatched on cancel-semantic closes (cancel button / mask / Esc), detail: { source } |
oas-close | Close: cancel button / ✕ / mask click / Esc, detail: { source } |
oas-closed | [Compat alias] Close animation finished, same as oas-after-close; will be removed later |
oas-ok | Clicked "OK" |
oas-open | Emitted when opening starts (after the scroll lock, before initial focus); no detail |
oas-opened | [Compat alias] Open animation finished, same as oas-after-open; will be removed later |
oas-resize | Emitted after drag-resize / arrow-key adjust / snap; detail: { size } (current main-axis size in px) |
Slots
| Name | Description |
|---|---|
| default | Drawer body content |
footer | Footer action area |
header-actions | Action area on the right of the title bar |
title | Rich title content slot; overrides the title attribute text when present |
CSS Variables
| CSS Variable | Default |
|---|---|
--oas-glass-blur | none |
--oas-glass-ring | transparent |
role="dialog" + aria-modal="true"; focus moves in on open (default ✕, overridable via initial-focus) and is restored on close.