Drawer
ProPremium slide-over panel from any edge with 6 semantic variants, 6 sizes, overlay/push modes, built-in variant-styled footer actions, loading & non-modal states, and ESC/backdrop close.
</>sv-drawer drawerpaneloverlayvariantsheetpush
import { SvDrawerComponent } from '@styloviz-pro/overlays';Demo & usage
Basic usage
ProUsage example — open the gallery for the full interactive demo.
Pro
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Back closes it, by default
ProGuard an unsaved form
ProDanger variant with subtitle
ProBottom sheet
ProLoading during async save
ProNon-modal side panel
ProPush content aside
ProModal, but not dimmed
ProBuilt-in footer actions
ProShow / hide header & footer
ProWhen to use
- Showing secondary content or forms alongside the page without full navigation.
- Filters, details, settings, carts — anything that slides over and dismisses.
- Semantic panels (danger, success, warning) — reach for the matching `variant`.
- For a centered, attention-grabbing dialog use a Modal.
Async states
- loading
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| open | boolean (two-way) | false | Controls drawer visibility. Use [(open)] for two-way binding. |
| position | DrawerPosition | 'right' | Edge from which the drawer slides in. |
| size | DrawerSize | 'md' | Width (left/right) or height (top/bottom). |
| variant | DrawerVariant | 'default' | Semantic/visual variant. Drives the header icon badge + accent bar. |
| title | string | '' | Optional title in the default header slot. |
| subtitle | string | '' | Optional secondary line beneath the title (also used for aria-describedby). |
| ariaLabel | string | 'Drawer' | Accessible label used when no `title` is provided. |
| showHeader | boolean | true | Render the built-in header bar. |
| showHeaderDivider | boolean | true | Bottom dividing line under the header. |
| showFooter | boolean | true | Render the footer slot. |
| showIcon | boolean | true | Show the built-in variant icon badge in the header. |
| showClose | boolean | true | Show the close (×) button. |
| showBackdrop | boolean | null | null | Render the dimming backdrop (set false for a non-modal drawer). |
| modality | DrawerModality | null | null | Which contract this drawer follows. |
| backdropBlur | boolean | true | Apply a frosted-glass blur to the backdrop. |
| closeOnBackdrop | boolean | true | Close when the backdrop is clicked. |
| closeOnEsc | boolean | true | Close on the ESC key. |
| loading | boolean | false | Busy state. While `true` the panel shows a spinner overlay and all close paths (button, backdrop, ESC) are blocked so a running task can finish. |
| pendingDismissal | SvPendingDismissal | 'block' | May the user dismiss the drawer while `loading` is true? THE MODAL TWIN ANSWERED THIS AND THE DRAWER DID NOT. asks every async surface for a documented answer to "can the user dismiss this while it is in flight?" — Modal got `pendingDismissal` and `loadingCancelled`; Drawer kept a silent, hard-wired refusal, so a drawer whose request hung could not be closed by anything short of a reload. `'block'` keeps exactly today's behaviour and is the default. `'abort'` lets every close path through and emits `loadingCancelled` first, so the host can abort the work it started — the drawer does not own the request. |
| closeOnNavigation | boolean | true | Does a navigation close this drawer? A ROUTE CHANGE LEAVES THE DRAWER OPEN, over a page it has nothing to do with — and a drawer is worse than a modal here, because it is often non-modal: the user can carry on with the new page while a panel from the old one sits beside it. Takes effect once the app calls `provideSvOverlayRouteClose()` from `@styloviz/layout`; without it nothing changes. 's stack half landed in T1 — this is its history half. |
| backCloses | boolean | true | Does the browser Back button close this drawer? ON BY DEFAULT HERE, UNLIKE THE MODAL — and the asymmetry is the point. A drawer is usually a DESTINATION: a details panel for the row you clicked, a filter sheet you opened on purpose. Back is the gesture for "undo that", and on a phone it is the system gesture. A modal is more often a transient question, where Back should belong to the page. The mechanism is a marked `history` entry, not a route: the URL is untouched. |
| dirty | boolean | false | Is there unsaved work in this drawer?, AND A DRAWER IS THE WORST PLACE FOR THIS. It is the kit's preferred home for a long edit form — that is what the size and the scroll are for — and it is dismissed by a click anywhere in the large dim area beside it. One missed click discards the lot. THE SAME PROMPT AS THE MODAL'S, FROM THE SAME PLACE (`createCloseGuard`). The audit's warning was explicit: a "you have unsaved changes" prompt implemented four times will behave four ways. |
| canClose | ((reason: DrawerCloseReason) => boolean | void) | null | null | The host's own veto, consulted before anything else. Return `false` to refuse a close, or nothing to have no opinion. A veto is silent — it does NOT raise the unsaved-changes prompt, because the host refused for its own reasons and owns its own messaging. |
| unsavedTitle | string | 'Discard your changes?' | Heading of the unsaved-changes prompt. |
| unsavedMessage | string | 'This panel has changes that have not b… | Body of the unsaved-changes prompt. |
| discardLabel | string | 'Discard' | Label of the button that closes and loses the changes. |
| keepEditingLabel | string | 'Keep editing' | Label of the button that returns to the panel. |
| loadingText | string | 'Loading…' | Accessible label announced while `loading` is true. |
| initialFocus | string | '' | CSS selector for the element to focus first when the drawer opens (e.g. `'#search'`). Falls back to the first focusable element. |
| mode | DrawerMode | 'overlay' | Content relationship. `overlay` (default) floats above the page; `push` squeezes `pushTarget` aside so the panel and content sit side by side. |
| pushTarget | string | '' | CSS selector for the element pushed aside in `mode="push"` (e.g. `'#app-content-area'`). No-op when empty or in overlay mode. |
| primaryAction | string | '' | Built-in primary footer action label. Empty hides the button. |
| secondaryAction | string | '' | Built-in secondary footer action label. Empty hides the button. |
| customClass | string | '' | Extra CSS classes merged onto the panel. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| loadingCancelled | void | The drawer was dismissed while `loading` — by a close path under `pendingDismissal="abort"`, or by a navigation (which `loading` cannot hold back). Emitted BEFORE `closed`, so the host can stop the work. |
| opened | void | Emits when the drawer opens by any means. |
| closed | void | Emits when the drawer closes by any means. |
| dismissed | DrawerCloseReason | Emitted alongside `closed`, describing what triggered the close. A close caused by a navigation or a Back press reports `'programmatic'` here — the published union cannot grow — and its exact reason on `dismissedBy`. |
| dismissedBy | SvCloseReason | Emitted alongside `dismissed` with the exact reason, including `'route'` (the app navigated) and `'history'` (the Back button) from |
| discarded | DrawerCloseReason | The user discarded unsaved changes; the close follows. |
| closeCancelled | DrawerCloseReason | The user backed out of a close and returned to the panel. |
| primaryClick | void | Emitted when the built-in primary footer action is clicked. |
| secondaryClick | void | Emitted when the built-in secondary footer action is clicked. |
| openChange | boolean | Emits the new `open` whenever it changes — the output half of `[(open)]`. |
Customization & theming
- Six semantic variants with tinted header/footer, icon badge + accent bar; toggle the badge via `showIcon`.
- Open from any edge; six sizes (xs–full). Header and footer are independently optional via `[showHeader]`, `[showHeaderDivider]` and `[showFooter]` (all default `true`).
- Built-in variant-styled `primaryAction`/`secondaryAction` footer buttons, or fully custom `[slot=footer]`.
- Overlay or push mode (`mode="push"` + `pushTarget`), modal or non-modal (`showBackdrop`), backdrop blur, ESC/backdrop toggles, and a spinner `loading` state.
- Body scroll-lock with scrollbar-width compensation; `customClass` on the panel.
- Route and history integration: `[closeOnNavigation]` (default on) closes the panel when the app navigates — a drawer is often non-modal, so one left open beside a new page is worse than a stale modal — and `[backCloses]` is default ON here, unlike the Modal, because a drawer is usually a destination rather than a transient question. Wire `provideSvOverlayRouteClose()` for the route half.
- A controlled-close policy: `[dirty]` raises the same unsaved-changes prompt the Modal uses — over the panel, focused on Keep editing, closing afterwards with the reason the user actually acted on — and `[canClose]` is your own silent veto. `(discarded)` / `(closeCancelled)` report which way it went. One primitive for both families (`createCloseGuard` in `@styloviz/core`), because a prompt written twice drifts.
- Responsive: non-`full` sizes cap to `90vw` (left/right) or `90vh` (top/bottom) so the drawer never exceeds a phone viewport, leaving a sliver of backdrop to tap.
Best practices
- Do: Set `[dirty]` whenever the drawer holds a form — which is most of the time, since its width and scroll are what make it the right home for a long edit. It is dismissed by a click anywhere in the large dim area beside it, so without the guard one missed click discards the lot.
- Do: Choose an edge (`position`) that matches the content — right for details, bottom for mobile sheets.
- Do: Match the `variant` to intent, and use `[loading]` to block dismissal during async work.
- Do: Trap focus while open and restore it on close (handled for modal drawers).
- Do: When hiding the header with `[showHeader]="false"`, keep another dismissal path — a footer action, ESC, or backdrop click — since that removes the built-in close (×) button.
- Tip: Use `[showBackdrop]="false"` for persistent side panels that leave the page interactive.
Accessibility
- `role="dialog"`, `aria-modal` (when backdrop shown), labelled via `aria-labelledby` (title) and `aria-describedby` (subtitle); `aria-busy` while loading.
- `modality` decides the contract. A **modal** drawer traps Tab, marks the rest of the page `inert`, locks the page scroll, asserts `aria-modal="true"` and moves focus to `initialFocus` or the first focusable element. An **inline** one does none of those: the page beside it stays usable, so it does not take focus either unless you name an `initialFocus`. Focus is restored on close whichever it is — and if the trigger is gone by then, to the nearest thing that outlived it.
- ⚠️ `showBackdrop` used to decide all of that, which is why it is no longer the input to reach for: five behaviours branched on a name that described the dim layer. It now controls only the dim layer, and defaults to following the modality — so `[showBackdrop]="false"` on its own still gives you an inline drawer exactly as before. `modality` and `mode` are different axes: `mode` is overlay-vs-push (layout), `modality` is modal-vs-inline (may the page still be used).
- Hiding the header (`[showHeader]="false"`) removes the built-in close button — provide an alternative dismissal (footer action, ESC, or backdrop) so keyboard users can always exit.
- `opened`/`closed` fire for every transition; `dismissed` reports the reason (backdrop / escape / close-button / programmatic).
Feedback on Drawer
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.