Skip to main content
Urbicon UI

Drawer

Slide-in panel overlay from any viewport edge.

Playground

Placement
<script lang="ts">
  import { Drawer } from '@urbicon-ui/blocks';

  let open = $state(false);
</script>

<Drawer
  bind:open
  accentEdge
  intent="primary"
>
  <p>This is the drawer content. Try changing the placement and size controls.</p>
</Drawer>

01 When to use

Drawer is a <dialog>: always modal, with a backdrop and a focus trap. Use it for a transient panel that opens on user action, pulls focus, and closes when the action is done. The four placement values (left / right / top / bottom) cover side sheets, top notification bars, and mobile bottom-sheets.

Pick a different overlay if you need:

  • A side panel that is part of the page layout (persistent on desktop, no backdrop) → Sidebar.
  • A floating panel anchored to a specific trigger element (date picker, action list) → Popover.
  • A centered modal for confirmations or short forms → Dialog.

02 Examples

With Footer

bind:open drives the drawer from your own trigger. A title gives it a header with a built-in close button, and the footer snippet holds the actions.
<Button onclick={() => (footerOpen = true)}>Open with Footer</Button>
<Drawer bind:open={footerOpen} title="Confirm Changes">
  <p>Are you sure you want to apply these changes? This action cannot be undone.</p>
  {#snippet footer()}
    <Button variant="ghost" onclick={() => (footerOpen = false)}>Cancel</Button>
    <Button intent="primary" onclick={() => (footerOpen = false)}>Confirm</Button>
  {/snippet}
</Drawer>

Settings Panel

placement sets the edge the drawer slides from and size its width. Here, the right edge at sm.
<Button onclick={() => (settingsOpen = true)}>Open Settings</Button>
<Drawer bind:open={settingsOpen} title="Settings" placement="right" size="sm">
  <div class="space-y-4">
    <Input label="Display name" placeholder="Your name" />
    <Input label="Email" placeholder="name@example.com" type="email" />
    <Separator />
    <Toggle label="Email notifications" checked />
    <Toggle label="Dark mode" />
    <Toggle label="Analytics" checked />
  </div>
  {#snippet footer()}
    <Button variant="ghost" onclick={() => (settingsOpen = false)}>Cancel</Button>
    <Button intent="primary" onclick={() => (settingsOpen = false)}>Save</Button>
  {/snippet}
</Drawer>

Navigation Menu

A left-side navigation menu, the common mobile pattern. Each item closes the drawer when picked.
<Button onclick={() => (navOpen = true)}>Open Menu</Button>
<Drawer bind:open={navOpen} title="Menu" placement="left" size="sm">
  <nav class="flex flex-col gap-1">
    {#each ['Dashboard', 'Projects', 'Team', 'Settings', 'Help'] as item (item)}
      <button
        class="text-text-primary hover:bg-surface-hover rounded-modify px-3 py-2.5 text-left text-sm transition-colors"
        onclick={() => (navOpen = false)}
      >
        {item}
      </button>
    {/each}
  </nav>
</Drawer>

03 Customization

Frosted glass sheet

Tint the panel into frosted glass with class, wash the backdrop in colour, and neutralise the header and footer hairlines with slotClasses. The contain radius tier, slide animation, focus trap and scroll lock all stay. Raw colours because glass has no token equivalent.
<Drawer
  bind:open
  title="Filters"
  placement="right"
  size="sm"
  class="border-white/20 bg-white/10 text-white backdrop-blur-xl"
  slotClasses={{
    backdrop: 'bg-linear-to-br from-fuchsia-600/50 via-purple-600/50 to-indigo-700/50',
    header: 'border-white/15',
    title: 'text-white',
    closeButton: 'text-white/70 hover:bg-white/10',
    footer: 'border-white/15'
  }}
>

</Drawer>

This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.

04 Stacking & Nested Drawers

Several Drawers can be open at once: a wizard that opens a preview, which opens a calculation trace. Each renders through its own native <dialog>, so the browser stacks them in the top layer for you.

Stack order is LIFO

The most recently opened Drawer renders on top. Escape closes that topmost panel and leaves the ones beneath it open. Each Drawer keeps its own focus trap, so keyboard focus stays inside the top panel.

Backdrop and body scroll

Every open Drawer adds its own backdrop. The body-scroll lock is reference-counted: the page stays locked until the last Drawer closes, and closing the top panel hands keyboard control back to the one underneath.

Keep the depth to two or three

A wizard, then a preview, then a trace reads well; past three layers the panels crowd each other, especially on mobile. For a deeper flow, reach for a master-detail pattern (a list beside one replaceable detail panel) instead of nesting.

Vary the size on narrow screens

Each Drawer caps at 100dvw / 100dvh, so on a 320px viewport a stacked Drawer fills the screen and identical sizes merge into one panel. Give stacked Drawers different sizes (md then lg) so the layering stays legible.

Close every overlay at once

Drawer, Dialog, and the mobile Sidebar register with a shared overlayStack singleton on open. Reach for it when app-level cleanup spans an unknown overlay depth: logout, a route change, or an expired session. Desktop Sidebars stay out of the stack, since they are persistent layout rather than modal overlays.

overlayStack

import { overlayStack } from '@urbicon-ui/blocks';

// Close every open Drawer, Dialog, and mobile Sidebar (top-down)
overlayStack.closeAll();

// Inspect the stack
overlayStack.depth; // number
overlayStack.topId; // string | null

05 Accessibility

Focus Trap

When open, focus is trapped inside the drawer panel. Tab cycles through focusable elements. On close, focus returns to the element that triggered the drawer.

ARIA

Uses native <dialog> with aria-labelledby linked to the title and aria-describedby linked to the body content. The close button has an aria-label.

Keyboard

Escape closes the drawer (closeOnEscape) and so does a click on the backdrop (closeOnBackdropClick). Both are on by default.

Reduced Motion

The slide and fade run on the overlay duration tokens (--blocks-overlay-enter-duration / --blocks-overlay-exit-duration). Under prefers-reduced-motion they collapse to 1ms, so the drawer opens and closes in place.

06 API Reference

19 props
19 props 1 required
Prop
Type
Default
Description

07 Types

Local type definitions used by this component.

3 types
Name
Kind
Category
Used by
Description

08 Installation

Import

import { Drawer } from '@urbicon-ui/blocks';