Overlays

Drawer

Edge-anchored modal panel built on the native dialog element; slides from the left, right, or bottom edge for secondary navigation, settings, or mobile-first sheet patterns.

import { Drawer } from '@lostgradient/cinder/drawer';
overlaydialog
01

Overview

Edge-anchored overlay panel for supplementary content without leaving the current page. The placement prop picks the edge: left, right (default), or bottom for mobile-style bottom sheets.

Choosing this component

  • Showing detail or edit forms alongside a list or table where the user needs to stay in context.
  • Navigation trees, filter panels, or settings that the user may want to keep open while interacting with the page.
  • Secondary workflows that complement the current view rather than replacing it.
  • Mobile-first surfaces that should slide up from the bottom of the viewport — use placement="bottom", optionally with dragHandleVisible.

Choosing something else

  • Full-screen workflows that require the user's full attention — use a Modal or navigate to a new page.
  • Brief contextual explanations or single-action prompts — use a Popover instead.

Related components

  • Modal — blocking full-attention overlay when the user cannot continue without acting.
  • Sidebar — persistent side panel that is always visible (not overlaid).

Usage

svelte
<script lang="ts">
  import Button from '@lostgradient/cinder/button';
  import Checkbox from '@lostgradient/cinder/checkbox';
  import Drawer from '@lostgradient/cinder/drawer';

  let open = $state(false);
  let triggerRef: HTMLElement | null = $state(null);

  const filters = Array.from({ length: 30 }, (_, index) => ({
    id: `drawer-overflowing-filter-${index + 1}`,
    label: `Filter group ${index + 1}`,
  }));
</script>

<Button
  label="Open drawer"
  onclick={(event: MouseEvent) => {
    triggerRef = event.currentTarget as HTMLElement;
    open = true;
  }}
/>

<Drawer bind:open title="Filters" {triggerRef}>
  <div style="display: grid; gap: 0.75rem;">
    {#each filters as filter (filter.id)}
      <Checkbox id={filter.id} label={filter.label} />
    {/each}
  </div>

  {#snippet footer()}
    <Button variant="secondary" label="Cancel" onclick={() => (open = false)} />
    <Button label="Apply" onclick={() => (open = false)} />
  {/snippet}
</Drawer>
Live preview
02

When to use

Use when
  • Showing supplementary navigation, filters, or settings that should slide in from a page edge.
  • Presenting long-form content that benefits from a side panel without leaving the current view.
  • Presenting a focused task or set of actions that slides up from the bottom of the viewport on touch surfaces — use `placement="bottom"`.
Avoid when
  • Interrupting the user for a focused decision — use modal so the surface is centered and task-scoped.
  • Anchoring a small surface to a trigger — use popover instead.
03

Examples

Basic drawer

An edge-anchored slide-in panel with controls for placement, size, and triggerRef.

Bottom sheet

A bottom-placed drawer with a drag handle and enough content to scroll.

Overflowing drawer

A side drawer with a scrollable body.

04

Props

Props for drawer
NameTypeDefaultDescription
open bindable boolean false Whether the drawer is open. Bindable via bind:open.
placement 'left' | 'right' | 'bottom' 'right' Edge the drawer slides in from.
size 'sm' | 'md' | 'lg' | 'xl' 'md' Drawer width token for left/right placements. Ignored for placement="bottom", which always spans the full viewport width and caps its height at 90dvh.
title required text Accessible name for the drawer. Required for screen-reader labelling. Rendered as a visible <h2> in the default header. When a custom header snippet is provided without ariaLabelledby, this text is rendered in a visually-hidden <h2> as the accessible name fallback.
triggerRef HTMLElement | null null Optional reference to the element that opened the drawer. When supplied, focus returns to this element on close. When omitted, focus restores to the element that held focus before the drawer opened.
ariaLabelledby text Optional id of an element that names the drawer. When supplied, drawer wires aria-labelledby to this id and renders no internal heading. Use this when a custom header snippet has its own visible heading — supply ariaLabelledby pointing to that heading's id so the visible and accessible names stay in sync.
dragHandleVisible boolean false When true and placement="bottom", render a decorative drag handle above the header. Swipe-to-close gesture is a stretch goal not implemented in MVP — the handle is purely a visual affordance. Ignored for left/right placements. Named dragHandleVisible (not draggable) to avoid colliding with the native HTML draggable attribute on the underlying <dialog>.
header snippet Custom header. Falls back to a default header that renders title.
children required snippet Drawer body content. Required.
footer snippet Optional footer (e.g. action buttons).