Skip to content
FelicityUI

Drawer

An edge-attached panel for longer content. Native presentation on every platform; shared intent, tokens, and naming.

Edit profile

Make changes to your profile here. Click save when you're done.

Installation

terminal
npx felicityui add drawer --platform react

Usage

example.tsx
import { Button } from "@/components/ui/button"
import { Drawer, DrawerBody, DrawerFooter } from "@/components/ui/drawer"

<Drawer
  open={open}
  onOpenChange={setOpen}
  title="Edit profile"
  description="Make changes to your profile here."
>
  <DrawerBody>{/* fields */}</DrawerBody>
  <DrawerFooter>
    <Button variant="ghost" onClick={() => setOpen(false)}>
      Cancel
    </Button>
    <Button onClick={() => setOpen(false)}>Save changes</Button>
  </DrawerFooter>
</Drawer>

Composition

Use the following composition to build a Drawer.

Drawer
├── title
├── description
├── handle (showHandle)
└── children
    ├── DrawerBody
    └── DrawerFooter

React uses a native <dialog> parked on an edge. SwiftUI uses .sheet with detents. Compose uses ModalBottomSheet. Those are different objects on purpose. There is no Vaul, Base UI, or shared overlay runtime.

Use Drawer when the work is longer than a two-button confirm. Use Dialog when it is not.

On the web, side parks the panel on an edge and className can set h-* / max-h-* or w-* / max-w-*. SwiftUI and Compose still present a system bottom sheet — do not clone a left web drawer onto iPhone.

When to use

  • Edit a profile, apply filters, or review a list that should scroll.
  • A bottom sheet or a side panel that still blocks the page.
  • Content with a sticky footer of actions.

When not to use

  • A short confirm or acknowledge. Use Dialog.
  • App chrome navigation. Use the platform's navigation primitives (NavigationSplitView, ModalNavigationDrawer as a scaffold, not this overlay).
  • A tooltip or inline help.

Default

Bottom sheet for longer work. Native <dialog> on the web, .sheet on iOS, ModalBottomSheet on Android.

Edit profile

Make changes to your profile here. Click save when you're done.

Position

side attaches the web panel to an edge. SwiftUI and Compose still present a bottom sheet — do not clone a left web drawer onto iPhone.

Filters

Narrow the list. Native sheets stay a bottom sheet.

Swipe handle

The handle is a drag affordance on a bottom sheet. Drag down to dismiss on the web. Native sheets use the system indicator.

Comments

Drag the handle down to dismiss.

Maya Chen: Ship Drawer as a native sheet, not a web card.

Reviewer: Bottom on iOS and Android. Sides are a web adaptation.

Scrollable content

DrawerBody scrolls while the title and DrawerFooter stay put. The scrollbar is hidden; the list still scrolls.

Activity

The list scrolls. Title and actions stay put.

  • Update 1
    Unpublished change · 1 hour ago
  • Update 2
    Unpublished change · 2 hours ago
  • Update 3
    Unpublished change · 3 hours ago
  • Update 4
    Unpublished change · 4 hours ago
  • Update 5
    Unpublished change · 5 hours ago
  • Update 6
    Unpublished change · 6 hours ago
  • Update 7
    Unpublished change · 7 hours ago
  • Update 8
    Unpublished change · 8 hours ago
  • Update 9
    Unpublished change · 9 hours ago
  • Update 10
    Unpublished change · 10 hours ago
  • Update 11
    Unpublished change · 11 hours ago
  • Update 12
    Unpublished change · 12 hours ago
  • Update 13
    Unpublished change · 13 hours ago
  • Update 14
    Unpublished change · 14 hours ago
  • Update 15
    Unpublished change · 15 hours ago
  • Update 16
    Unpublished change · 16 hours ago
  • Update 17
    Unpublished change · 17 hours ago
  • Update 18
    Unpublished change · 18 hours ago

Non-modal

modal={false} uses dialog.show() on the web so the page stays interactive. Native sheets remain modal.

Inspector

The page behind stays interactive. Native sheets remain modal.

RTL

Pass dir="rtl" on the React drawer. SwiftUI and Compose follow the system layout direction.

الفلاتر

ضيّق القائمة.

API Reference

Drawer

PropTypeDefaultDescription
open / isPresentedboolean / Binding<Bool>Whether the drawer is shown. SwiftUI uses isPresented.
onOpenChange / onDismiss(open: boolean) => void / () -> UnitCalled when the user dismisses. React uses onOpenChange; Compose uses onDismiss.
titlestringAccessible name. Required.
description / message / textstringOptional supporting copy. Named description (React), message (SwiftUI), text (Compose).
side"bottom" | "top" | "left" | "right"bottomEdge the panel attaches to on the web. SwiftUI and Compose present a bottom sheet for every value.
showHandle / showsDragIndicatorbooleantrue when side is bottomDrag handle. React defaults true for bottom. SwiftUI maps to presentationDragIndicator.
modalbooleantrueWhen false, React uses dialog.show() with no backdrop and no scroll lock. Native sheets stay modal.
children / contentReactNode / @ViewBuilder / @ComposableBody and footer. Compose Button here on React; Drawer does not import it.
confirmLabel / cancelLabelstring | nullDone / CancelAction labels on Compose. Omit cancel with null.
onConfirm() -> UnitConfirm handler on Compose.
classNamestringAdditional classes on the dialog element.
dir"ltr" | "rtl"Text direction on the React drawer. Native platforms follow the system layout direction.

Accessibility

WCAG 2.2 AA

  • Must be exposed as a dialog (or the platform sheet equivalent), not a custom overlay div.
  • Title is the accessible name.
  • Description, when present, is the accessible description.
  • Focus moves into the drawer when it opens and returns to the invoker when it closes.
  • Escape, the platform dismiss gesture, or a drag-to-dismiss on a bottom sheet closes a non-required drawer.
  • Actions are real buttons with visible names.

Platform notes

React

Renders a native HTML <dialog> with showModal(), positioned on an edge. Not a portal library. Bottom sheets can be dragged down to dismiss.

  • Client Component: showModal() must run in the browser.
  • Compose actions with Button in children. Drawer does not import Button.
  • side is bottom, top, left, or right. Default bottom.
  • showHandle defaults to true for bottom. It is a visual affordance; drag the sheet to dismiss.
  • modal={false} uses dialog.show() so the page stays interactive and has no backdrop.
  • DrawerBody scrolls; DrawerFooter stays put. Hide the scrollbar; keep scrolling.
  • Document scroll is locked while a modal drawer is open.

SwiftUI

SwiftUI .sheet with medium and large detents. Not a web-style side card. side is accepted for API parity; iOS presents a system sheet.

  • VoiceOver announces the system sheet title.
  • Dynamic Type is the system sheet type size.
  • presentationDragIndicator maps to showHandle.
  • Leading, trailing, and top still present as .sheet. Do not clone a left web drawer onto iPhone.

Compose

Material3 ModalBottomSheet with FelicityUI colors. Scrim, back, and TalkBack stay platform-native. side is accepted for API parity; Android presents a bottom sheet.

  • TalkBack announces the title.
  • Back and scrim call onDismiss.
  • Start, end, and top still present as ModalBottomSheet. A persistent navigation drawer belongs in the app scaffold, not this overlay.