SvDrawer

An edge-anchored side sheet - right, left, top, or bottom - with a backdrop and the full dialog accessibility contract.

SvDrawer slides a panel in from any edge and is built entirely on the shared primitives (createFocusTrap, lockScroll, createDismissableLayer), so its focus, scroll-lock, and Escape/backdrop behaviour is identical to SvModal and nested overlays close top-first. Set sheet for a mobile bottom-sheet with a grab handle and swipe-down-to-close. Controlled via a bindable open.

Related: SvModal · SvToaster · Overlays & menus overview

Installation

Add it with the CLI - this drops a ready-to-edit SvDrawer starter into your app:

Prefer to see it first? npx @svgrid/ui try drawer opens it in a throwaway sandbox - no project needed.

Or install the package and import it directly. SvDrawer ships free in @svgrid/grid (dependency-free):

The examples on this page import from @svgrid/grid:

<script lang="ts">
  import { SvButton, SvDrawer } from '@svgrid/grid'
</script>
import { SvDrawer } from '@svgrid/grid'

Example

Open the live example: App overlays: drawer, context menu, toasts (Layout)

<script lang="ts">
  import { SvDrawer, SvButton } from '@svgrid/grid'
  let open = $state(false)
</script>

<SvButton onclick={() => (open = true)}>Filters</SvButton>

<SvDrawer bind:open side="right" title="Filters">
  <p>Body content.</p>
  {#snippet footer()}<SvButton variant="primary" onclick={() => (open = false)}>Apply</SvButton>{/snippet}
</SvDrawer>

Props

Prop Type Default Description
open boolean false Controlled, bindable open state.
onClose () => void - Called whenever the drawer closes.
onClosed () => void - Fired after the exit animation ends and the drawer leaves the DOM.
side right | left | top | bottom right Edge the drawer slides from.
size string per-axis default Panel size along its axis; any CSS length.
title string - Header heading; also names the drawer for a11y.
closeOnBackdrop boolean true Close when the backdrop is clicked.
closeOnEsc boolean true Close on the Escape key.
hideClose boolean false Hide the header close (x) button.
sheet boolean false Render as a mobile bottom-sheet (overrides side).
ariaLabel string - Accessible name when there is no visible title.
children Snippet - Drawer body.
footer Snippet - Footer content, typically action buttons.

Examples

Record editor

The classic "click a row, slide in the record" flow - anchor right, give it a title, and put the save action in the footer:

<SvDrawer bind:open side="right" title="Edit contact" size="min(480px, 92vw)">
  <!-- form fields -->
  {#snippet footer()}
    <SvButton onclick={() => (open = false)}>Cancel</SvButton>
    <SvButton variant="primary" onclick={save}>Save</SvButton>
  {/snippet}
</SvDrawer>

Mobile bottom-sheet

Set sheet for a bottom-anchored sheet with a grab handle; users can swipe it down past the threshold to dismiss:

<SvDrawer bind:open sheet title="Sort by">
  <!-- options -->
</SvDrawer>

Row detail from a grid

Pair a drawer with a grid's row-select to build a master-detail view: keep the list visible on the left while the selected record slides in from the right. Track the row you opened and clear it on close:

<script lang="ts">
  import { SvDrawer, SvButton } from '@svgrid/grid'
  let selected = $state(null)
  const open = $derived(selected != null)

  function openRow(row) { selected = row }
</script>

<SvDrawer open={open} onClose={() => (selected = null)}
  side="right" title={selected?.name} size="min(480px, 92vw)">
  {#if selected}
    <dl>
      <dt>Email</dt><dd>{selected.email}</dd>
      <dt>Status</dt><dd>{selected.status}</dd>
    </dl>
  {/if}
  {#snippet footer()}
    <SvButton variant="primary" onclick={() => (selected = null)}>Done</SvButton>
  {/snippet}
</SvDrawer>

Tip: a drawer shares SvModal's dialog contract, so focus is trapped and page scroll is locked while it is open. For a persistent navigation rail that should not trap focus or dim the page, reach for a plain sidebar instead - a drawer is for a modal, dismissable surface.

Any edge

Choose side="left" for navigation, or top / bottom for banners and command surfaces; size accepts any CSS length along the panel's axis.

Accessibility

Which edge, and how much of it

side picks the edge and size the extent. A right drawer reads as a detail panel; a bottom one reads as a sheet on a phone, which is what sheet styles it as.

<script lang="ts">
  import { SvDrawer, SvButton } from '@svgrid/grid'

  let right = $state(false)
  let bottom = $state(false)
</script>

<SvButton onclick={() => (right = true)}>Open detail panel</SvButton>
<SvButton variant="outline" onclick={() => (bottom = true)}>Open sheet</SvButton>

<SvDrawer open={right} onClose={() => (right = false)} side="right" size="360px" title="Row detail">
  <p>Everything about the selected row.</p>
</SvDrawer>

<SvDrawer open={bottom} onClose={() => (bottom = false)} side="bottom" size="45%" sheet title="Actions">
  <p>The phone-shaped version of the same idea.</p>
</SvDrawer>

See also

Related articles