Skip to content

Drawer โ€‹

A side-anchored overlay panel that slides in from any of the four edges. Use it for detail views, filter panels, or contextual forms that don't deserve a full route. Bind open (or use the model-value alias) to control visibility, listen to toggle for state changes, and pick the slide direction with position. Toggle Vue / CSS to switch the live demo and source together.

The look is Basecoat's .drawer (see Theme): a bg-popover sheet pinned to one viewport edge that rounds and borders only the edge facing the content, with p-4 header / body / footer regions, over a bg-black/10 backdrop-blur-xs scrim, sliding on duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]. Two things changed with the port: the tinted gradient header bar and the tinted footer are gone (--mono-drawer-header-bg / -footer-bg and the matching -border / -border-width put them back), and the footer is now a stacked column, upstream's mobile-sheet idiom, which --mono-drawer-footer-direction: row reverses. The default ONE flavor sets that row back, so the pre-port action row survives; the tinted bars are restored nowhere, because dropping them is a decision of the port rather than a flavor's. Dark mode is built in.

Basic โ€‹

Default right-side drawer with a title, a subtitle (the muted description line under it โ€” hidden when empty), a body, and the close button.

Vue SFC

Positions โ€‹

Slide in from left, right, top, or bottom.

Vue SFC

Stacked โ€‹

Set stackable to let drawers stack โ€” open a drawer from inside another, any depth. Each new drawer sits above the previous, only one backdrop dims the screen, and Esc / overlay-click affect only the topmost. Without stackable a drawer is exclusive: opening one closes any other open drawer.

Vue SFC

Z-index โ€‹

By default the drawer takes its stacking level from mono's shared popup stack, so the newest layer is always on top. Set z-index to pin it instead โ€” for sitting above (or below) something the host app already positions, like a sticky header or a third-party widget. The value is the overlay's level; the panel renders one above it. Accepts z-index="1500", :z-index="1500" and :zIndex="1500".

Popups opened inside a pinned drawer โ€” a mono-select, a mono-button-dropdown, a table menu โ€” stack above it automatically: the shared stack resumes its chain from the pinned level rather than from its base. The lower-level --mono-drawer-z CSS variable does not get that treatment; set it and you own the whole chain.

Vue SFC

Sizes โ€‹

size sets the content scale โ€” title type, body type, padding and the close icon โ€” across xs, sm, md (default), lg, xl and xxl. The panel keeps the same width at every step; width / height take the same tokens when you want the measure to change too.

Changed

size used to be a dimension preset (sm 280px โ€ฆ full 100vw) and only partly affected typography. Use width / height for the panel measure; size="full" is now width="100%" (left/right) or height="100%" (top/bottom).

Vue SFC

Width & height โ€‹

Set width on a left/right drawer, or height on a top/bottom one โ€” the other axis is pinned to the viewport edge by position. Each accepts a preset token (xsโ€ฆxxl, below), a CSS length ("32rem", "60%", "100vw"), or a number read as px.

Dimension presets โ€‹

tokenwidth (left / right)height (top / bottom)
xs220px22vh
sm280px30vh
md420px โ€” the default50vh โ€” the default
lg560px70vh
xl720px85vh
xxl900px95vh

These are the drawer's long-standing dimensions, now reachable by name. The tokens are spelled the same as size but are a different axis: size scales the content, these name a measure โ€” <mono-drawer size="sm" width="xl"> is a compact, wide drawer. A token on the pinned axis is simply inert.

They are applied as --mono-drawer-width / --mono-drawer-height, the same channel the resize handle writes to, so resizeable keeps working on top of them.

Vue SFC

Auto full-screen โ€‹

Set auto-fullscreen and the drawer fills the screen on small viewports. It defaults to sm (under 640px); pass md, lg, xl or 2xl to move the boundary. The drawer keeps its position, and a resizeable one hides its handle while full-screen.

Scroll lock โ€‹

While open, a drawer locks page scroll โ€” lock-scroll, default true. Opt out with :lock-scroll="false" when the page behind should stay scrollable.

vue
<mono-drawer v-model="open" :lock-scroll="false" />

The lock is shared, not per-instance: overlapping overlays (a modal opened from a drawer, stacked modals, a sidebar) all draw on one lock, so scroll is released only when the last one that wanted it closes. mono-sidebar takes the same prop.

It is the document root that gets overflow: hidden (the body too, but that is not the part that matters). The body's overflow only reaches the viewport while the root's computed overflow is visible โ€” CSS Overflow ยง3.5 โ€” so any reset that gives html an overflow of its own cancels the propagation and a body-only lock does nothing at all. Vuetify's reset ships html { overflow-y: scroll }; ress and sanitize.css do the same. Locking the root is immune to it.

The declaration is written !important on the element itself, because a normal inline style loses to a stylesheet that claims the root's overflow with !important โ€” and apps do that: Vuetify's own scroll block is .v-overlay-scroll-blocked:not(html) { overflow-y: hidden !important }, and a layout that drives <html class> (.overflow-auto { overflow: auto !important }) pins it for the life of the page. That failure is silent in the worst way: documentElement.style.overflow reads "hidden", so every style assertion passes while the page keeps scrolling, because the computed value never changed. An important declaration in the style attribute outranks an important one from a selector, so it cannot be out-cascaded. Whatever was there before โ€” value and priority โ€” is put back exactly on close.

The scrollbar goes away with it, and its width is handed back to the body as padding-right so nothing shifts sideways โ€” measured first, because a system with overlay scrollbars has no width to give back and compensating there would itself be the shift. Deliberately not scrollbar-gutter: stable, which reserves the space in one declaration but leaves Chrome painting the empty scrollbar track: the page then still looks like it has a scrollbar, which is usually the whole complaint.

iOS

overflow: hidden does not stop touch scrolling in iOS Safari. Locking there needs position: fixed plus scroll-position save/restore, which brings its own problems (lost position, focus jumps), so the drawer does not attempt it.

Resizable โ€‹

Set resizeable to let users resize the drawer by dragging the handle on the panel's inner edge โ€” width for left/right drawers, height for top/bottom. The size is clamped between a minimum and the viewport.

Vue SFC

Colors โ€‹

Pick a colour, then open the drawer: the panel's edge and a faint glow under it take the colour, as do the resizer and the close-button hover โ€” it re-paints live, so re-pick while it is open. Without color the edge stays neutral and there is no glow.

Vue SFC

The header is a row: a heading column (title over subtitle) and the close โœ• beside it โ€” the same model as mono-modal. The slots are:

SlotAliasRenders
headerโ€”Replaces the whole heading column โ€” title and subtitle. Beats the title / subtitle slots and props. The โœ• stays
titleโ€”Replaces the title line (beats the title prop)
subtitleโ€”Replaces the subtitle line (beats the subtitle prop)
body(default slot)Main content โ€” unslotted children land here
footerfootAction area under the body

The header shows whenever there is a title, a subtitle, a header slot or the โœ•; :dismissible="false" drops the โœ•.

Changed

In the light build slot="title" used to be a second spelling of header. It is now the real title slot โ€” it replaces only the title line, so a subtitle still shows under it. The shadow build now supports title and subtitle too (it only knew header before).

Vue SFC

Persistent โ€‹

Overlay click and Escape are ignored โ€” only the close button or programmatic hide() will dismiss.

Vue SFC

No overlay โ€‹

Side panel without a backdrop โ€” page behind stays interactive. A click outside the panel still closes the drawer (close-on-overlay, on by default) and reaches the page; :close-on-overlay="false" keeps it open.

Vue SFC

Event log โ€‹

Live log of toggle, open and close events with source (overlay / close / escape / manual).

Vue SFC

Customized โ€‹

Per-element overrides via cssClass (Vue) or utility classes (CSS).

Vue SFC

CSS Variables โ€‹

Vue SFC

Every drawer is themed through --mono-drawer-* custom properties. Because the panel renders through a <body> portal, --mono-drawer-* set inline on the <mono-drawer> element are forwarded to the portal (per-instance theming); you can also set them on :root to theme every drawer, and they pierce the shadow-DOM boundary for the shadow build. The size / position / color props set presets, but an explicit --mono-drawer-* override always wins. To re-skin globally, set the underlying tokens (--popover, --border, --mono-mode-backdrop โ€ฆ) or switch flavor. Scroll areas use the shared --mono-scrollbar-* tokens.

VariableDefaultControls
--mono-drawer-bg (alias -surface) / -text--popover / --popover-foregroundThe panel's fill and ink
--mono-drawer-border / -border-width--border / --mono-border-widthThe one edge that faces the content
--mono-drawer-shadow--mono-shadow-lgThe panel's elevation. luma and rhea raise it to --mono-shadow-xl
--mono-drawer-accent-border / --mono-drawer-glowaccent 35% on --border / a 24px halo of the accent at 45%Only while color is set: the edge becomes a thin line of the colour and a faint glow of it sits under the elevation
--mono-drawer-radius / -radius-<size>--mono-radius-xl at mdThe two corners on the content-facing edge. lyra and sera square them off
--mono-drawer-inset / -inset-radius / -inset-border-width / -panel-pad0 / inherit / 0 / 0Float the panel as a CARD inset from the viewport edge. maia, mira, luma and rhea are exactly this
--mono-drawer-pad-<size>4 ร— --mono-spacing at md (p-4)The padding each of the three regions brings
--mono-drawer-body-padfollows -pad-<size>The body alone โ€” set it to 0 for upstream's bare > section
--mono-drawer-width / -height / -max-heightmin(75%, --mono-container-sm) / auto / 80vhThe measure โ€” not set by size; use the width / height props (a length, or an xsโ€ฆxxl token)
--mono-drawer-title-font-<size> / -title-weight / -title-tracking / -title-line-height / -title-transform / -title-color--mono-text-lg / semibold / -0.015em / --mono-leading-none / none / the panel inkThe title. lyra and mira drop it to text-sm font-medium; sera sets it uppercase tracking-wider
--mono-drawer-subtitle-font / -subtitle-color / -subtitle-line-heightthe body font (--mono-text-sm at md) / --muted-foreground / --mono-leading-normalThe subtitle โ€” Basecoat's drawer description (text-muted-foreground text-sm)
--mono-drawer-body-font-<size> / -body-line-height--mono-text-sm at mdThe body copy. lyra and mira use text-xs/relaxed
--mono-drawer-header-gap-<size> / -footer-gap-<size>1.5 ร— / 2 ร— --mono-spacing at mdInside the header row and the footer stack
--mono-drawer-heading-gapthe header gapBetween the title and the subtitle
--mono-drawer-footer-direction / -footer-align / -footer-justifycolumn / stretch / flex-startThe footer is upstream's STACKED sheet. row / center / flex-end is the pre-port action row, which ONE sets
--mono-drawer-header-bg (alias -head-bg) / -header-border / -header-border-widthtransparent / transparent / 0The header BAR โ€” off, because upstream has none. Set the three to bring the pre-port bar back
--mono-drawer-footer-bg (alias -foot-bg) / -footer-border / -footer-border-widthall offLikewise for the footer bar
--mono-drawer-close-size-<size> / -close-glyph / -close-radius / -close-bg / -close-color / -close-opacity / -close-hover-opacity / -close-hover-bg7 ร— --mono-spacing / size-4 / --mono-radius-sm / transparent / the panel ink / 0.7 / 1 / transparentThe โœ• (an EXTENSION โ€” upstream's drawer has no close button; it borrows the dialog's)
--mono-drawer-overlay-bg / -overlay-backdrop-filter--mono-mode-backdrop (black/10) / blur(--mono-blur-xs)The scrim
--mono-drawer-accent / -<role>--primary / the ten rolesThe colour the resizer, a restored bar and the โœ• hover draw from (set by color)
--mono-drawer-duration / -ease500ms / cubic-bezier(0.32, 0.72, 0, 1)The slide; 0s for an instant, animation-free drawer
--mono-drawer-z9990Base z-index (the popup stack overwrites this per level; prefer the z-index prop, and note an inline var still wins over it)

Types โ€‹

Importimport { DrawerProps } from 'mono-helper'
PropValueDefaultDescription
modelValuebooleanโ€”Two-way bound open state of the drawer.
position'left' | 'right' | 'top' | 'bottom'โ€”Edge the drawer slides in from.
size'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'โ€”Sizing scale of the drawer panel.
color'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'teal' | 'purple' | 'neutral' | 'dark'โ€”Color theme applied to the drawer.
titlestringโ€”Heading text shown in the drawer header. `slot="title"` replaces it.
subtitlestringโ€”Secondary line under the title โ€” muted and smaller, hidden when empty. `slot="subtitle"` replaces it; `slot="header"` replaces title + subtitle together (the close โœ• stays).
dismissiblebooleanโ€”Shows a close button and allows dismissal.
persistentbooleanโ€”Prevents closing via overlay click or escape.
overlaybooleanโ€”Renders a backdrop overlay behind the panel.
closeOnEscapebooleanโ€”Closes the drawer when the Escape key is pressed.
closeOnOverlaybooleanโ€”Closes the drawer on a click outside the panel: the overlay, or โ€” with `overlay: false` โ€” the page itself (the click still reaches the page).
lockScrollbooleanโ€”Locks page scroll while the drawer is open (default `true`). The lock is shared across overlapping overlays, so it lifts only when the last one closes.
autoFullscreen'sm' | 'md' | 'lg' | 'xl' | '2xl' | booleanโ€”Fill the screen at a breakpoint and below (default `false`). `auto-fullscreen` on its own means Tailwind's `sm` (< 640px); a token moves the boundary, so `auto-fullscreen="lg"` covers everything `lg:` does not match (< 1024px).
resizeablebooleanโ€”Allow resizing the drawer by dragging a handle on the panel's inner edge.
stackablebooleanโ€”Allow this drawer to stack on top of others. When false (default) it is exclusive โ€” opening it closes any other open drawers.
widthnumber | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | string & {}โ€”Panel dimensions. Each accepts a preset token (`"xs"`โ€ฆ`"xxl"` โ€” see {@link DrawerDimensionPreset}), a CSS length string (`"12rem"`, `"80%"`, `"100vw"`), or a number / numeric string (interpreted as px). `width` applies to left/right drawers, `height` to top/bottom ones โ€” the other axis is pinned to the viewport edge by `position`. `width="100%"` (or `height="100%"`) replaces the old `size="full"`.
heightnumber | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | string & {}โ€”โ€”
zIndexstring | numberโ€”Pin the drawer to an explicit stacking level. Unset (the default) lets the shared popup stack assign one so the newest layer is always on top. The value is the overlay's `z-index`; the panel sits one above it.
cssClassDrawerCssClassโ€”Per-element class overrides for internal parts.
cssClassNamestringโ€”Extra class name applied to the drawer root.