Drawer#
Component <neo-drawer> is a modal side-sheet on the platform <dialog>. Same browser-owned modal lifecycle as Dialog, anchored to one of four viewport edges via side (right default, left / top / bottom) and animated in with a slide transform. Dismiss via backdrop, Esc, or swipe toward the anchored edge; set dismissible="false" to force an explicit button choice.
The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.
Signals
Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.
Reference#
Attributes #
| Name | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Controls drawer visibility. |
side | "right" | "left" | "top" | "bottom" | right | Edge the drawer slides in from. |
dismissible | true | false | true | Backdrop click and Esc close the drawer. Set dismissible="false" to require an explicit close. |
touch-dismiss | "off" | CSS length | on | Drag-to-close gesture. On by default; set touch-dismiss="off" (or "false" / "no" / "none") to disable. The default close threshold is half the panel's relevant dimension along the swipe axis; pass a CSS length (e.g. 120, 120px, or 40%) to pin it explicitly. Touches that start on a same-axis scrollable child or on an element that owns a same-axis gesture (range input, slider, resizable handle, color field, or any [data-neo-drawer-touch-ignore] descendant) are ignored. Disabled when dismissible="false". |
States #
| Name | Description |
|---|---|
:state(dragging) | Set while a touch-dismiss drag is in flight. |
Slots #
| Name | Description |
|---|---|
| default | A <dialog> with optional [data-neo-drawer-header], [data-neo-drawer-body], [data-neo-drawer-footer] children. |
[data-neo-drawer-title] | Accessible name for the drawer; its id wires the dialog's aria-labelledby. |
[data-neo-drawer-description] | Accessible description; its id wires the dialog's aria-describedby. |
[data-neo-drawer-trigger] | Element that opens the drawer on click. |
[data-neo-drawer-close] | Element that closes the drawer on click (matched via closest, so a descendant works too). |
[data-neo-async-placeholder] | Loading skeleton swapped in while the body morphs in. |
Events #
| Name | Detail | Bubbles | Description |
|---|---|---|---|
neo-drawer-open | none | yes | Fires when the drawer opens. |
neo-drawer-close | none | yes | Fires when the drawer closes. |
CSS variables #
| Name | Default | Description |
|---|---|---|
--neo-drawer-width | 22rem | Panel width for the left and right sides. |
--neo-drawer-height | 16rem | Panel height for the top and bottom sides. |
--neo-drawer-enter-duration | calc(220ms * var(--neo-duration-scale, 1)) | Slide and fade transition duration. Surface colors, border, shadow, and padding reuse the --neo-dialog-* tokens. |
Examples#
Sides#
Same markup, different side. The panel anchors to that edge and slides in along the matching axis; width vs height defaults swap automatically.
Frost glass#
Theme the surface by setting --neo-dialog-* tokens inline on the host.
- The drawer reuses the dialog's surface tokens, so any recipe that frosts a centred dialog frosts the side sheet too.
- Here a 22px backdrop blur on the surface plus a 6px overlay blur read through both layers as a deep frosted scene.
Non-dismissible#
For must-decide flows where dismissing implicitly would be ambiguous, set dismissible="false" on the host: backdrop click and Esc no longer close, so the user must pick a button.
Scrolling body#
The body region scrolls independently while the header and footer stay pinned, common for a settings drawer with many fields.
Touch-dismiss#
Mobile-style drag-to-close, on by default. A single-touch swipe in the closing direction follows the finger; release past the threshold or a fast flick closes, otherwise it snaps back.
- Axis follows
side: left/right drawers take horizontal swipes, top/bottom vertical. - The threshold defaults to half the panel's relevant dimension; pass a CSS length (here
touch-dismiss="120px") to pin it, ortouch-dismiss="off"to opt out. - Try the preview on a touchscreen, or with browser device-toolbar touch emulation.
The gesture stands down for descendants that own a same-axis gesture of their own (same-axis scrollable containers, range inputs, sliders, resizable handles, color fields, or any element marked [data-neo-drawer-touch-ignore]), and is disabled when dismissible="false". The <neo-slider> in the panel below demonstrates this: dragging the slider thumb horizontally never closes the drawer.
Opened from a dialog#
Open a drawer from inside a dialog. The dialog's Connected devices… button triggers a nested <neo-drawer> that slides in over the modal, so both overlays are open together; each keeps the page behind it from scrolling while its own content scrolls. Open the dialog, then click Connected devices….
Examples: Datastar#
Load the body on every open. A [data-neo-async-placeholder] child holds skeleton bars; opening posts to the server, whose response morph-patches real content in to replace it.
- On close the host reinstates the placeholder after the slide-out finishes (so it doesn't flash through the closing panel), so each open shows it again until the next response resolves.
- The editor below is bound to the
drawer_async_htmlsignal the server reads at request time; edit it to change the next response.
Async loading#
Loading states only show with simulated delay. Raise network latency or handler delay in the Settings panel.
Async load with failure swap#
Async load that shows a failure state and a retry button when the request keeps failing.
- The loader lives in the drawer's
[data-neo-async-placeholder]slot, so close/reopen restores it; on success the server morphs<div id="<host-id>-body">in place. - Once Datastar exhausts its retry budget, a failure view with a
[data-neo-popover-async-retry]button replaces the body without closing the drawer. - Same failure-swap contract as PopoverAsync.
Roman Scharkov
Builds Morpheus, the server-driven web component kit this site is built with.
Daniel Scharkov
Works on Morpheus with his brother Roman; the two ship it together.
Loading states only show with simulated delay. Raise network latency or handler delay in the Settings panel.