# Drawer

> Mobile bottom sheet on Base UI Dialog with a drag handle and swipe-down-to-close; snaps back below the threshold and respects reduced motion.

Source: https://docs.nasaqui.com/components/drawer

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/drawer.json
```

A modal panel that slides up from the bottom edge, made for touch screens. It has a drag handle: drag it down and the drawer follows your finger. Release past 30% of its height, or with a quick flick, and it closes; release earlier and it snaps back. It is a Base UI Dialog, so focus is trapped, `Esc` and the backdrop close it, and the title and description are wired for screen readers.

## Drawer versus Sheet

| | Drawer | Sheet |
| --- | --- | --- |
| Edge | Bottom only | `end`, `start` or `bottom` |
| Gesture | Drag handle, swipe down to close | None; buttons, `Esc`, backdrop |
| Best for | Mobile actions, pickers, short forms | Desktop side panels, settings, notifications |
| Width | Full width, up to `max-w-xl`, centred | 24rem side panel |

Pick **Drawer** when the primary device is a phone and the user expects to swipe it away. Pick **Sheet** for side-over panels on wide screens; its `side="bottom"` variant is a static panel without the gesture. Both can be used in one app: `Sheet` at `md` and up, `Drawer` below.

## When to use

- Contextual actions, filters or a short form on mobile.
- Any bottom panel that should feel native to a touch UI.

## When not to use

- A side panel or a desktop-first surface: use [`Sheet`](https://docs.nasaqui.com/components/sheet).
- A blocking question or a confirmation: use `Dialog` or `AlertDialog`.
- Small anchored content: use [`Popover`](https://docs.nasaqui.com/components/popover).

## Import

```tsx
import { Drawer, DrawerContent /* … */ } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import {
  Button, Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,
} from "@fadymondy/nasaq/web";

export function Example() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="secondary" />}>Open drawer</DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Edit project</DrawerTitle>
          <DrawerDescription>Drag the handle down to dismiss.</DrawerDescription>
        </DrawerHeader>
        <DrawerBody className="p-4">Content</DrawerBody>
        <DrawerFooter className="justify-end">
          <DrawerClose render={<Button variant="ghost" />}>Cancel</DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  );
}
```

## Anatomy

```
Drawer                        state owner (open / defaultOpen / onOpenChange), Base UI Dialog.Root
├─ DrawerTrigger              Dialog.Trigger
└─ DrawerContent              data-slot="drawer-content", data-dragging while dragging   (portal + backdrop + popup)
   ├─ handle                  data-slot="drawer-handle"     the only drag surface
   ├─ DrawerHeader            data-slot="drawer-header"
   │  ├─ DrawerTitle          data-slot="drawer-title"
   │  └─ DrawerDescription    data-slot="drawer-description"
   ├─ DrawerBody              data-slot="drawer-body"       scrolls
   ├─ DrawerFooter            data-slot="drawer-footer"
   └─ close button            data-slot="drawer-close"
```

## API

### `Drawer`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open?` | `boolean` | uncontrolled | Controlled open state. |
| `defaultOpen?` | `boolean` | `false` | Initial state when uncontrolled. |
| `onOpenChange?` | `(open: boolean) => void` | none | Called on every open or close, including swipe. |
| `children?` | `ReactNode` | none | Trigger and content. |

### `DrawerContent`

`DrawerContentProps extends ComponentProps<typeof BaseDialog.Popup>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `showHandle?` | `boolean` | `true` | Shows the handle and enables swipe-down-to-close. |
| `showClose?` | `boolean` | `true` | Shows the × button. |
| `closeLabel?` | `string` | `"Close"` / `"إغلاق"` by locale | Accessible name of the × button. |
| `className?` | `string` | none | Merged onto the popup. |

`DrawerTrigger` and `DrawerClose` are the Base UI `Dialog.Trigger` and `Dialog.Close` (use `render` to make them Buttons). `DrawerHeader`, `DrawerBody`, `DrawerFooter` are `div`s; `DrawerTitle` and `DrawerDescription` wrap the Base UI parts.

## Examples

### Controlled

```tsx
import { Button, Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Controlled() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button onClick={() => setOpen(true)}>افتح الدرج</Button>
      <Drawer open={open} onOpenChange={setOpen}>
        <DrawerContent closeLabel="إغلاق">
          <DrawerHeader>
            <DrawerTitle>تعديل المشروع</DrawerTitle>
          </DrawerHeader>
        </DrawerContent>
      </Drawer>
    </>
  );
}
```

## Accessibility

| Key | Action |
| --- | --- |
| `Esc` | Closes the drawer. |
| `Tab` / `Shift+Tab` | Cycles focus inside the drawer (trapped). |
| `Enter` / `Space` on the × button | Closes it. |

- Swiping is a pointer-only enhancement. The handle is `aria-hidden`; the × button, `Esc` and the backdrop are the accessible ways to close.
- Only the handle drags, so scrolling the body never closes the drawer by accident.
- With `prefers-reduced-motion: reduce`, the slide animations are off and a swipe past the threshold closes immediately.
- Give every drawer a `DrawerTitle`. Localise `closeLabel` for languages other than English and Arabic.

## RTL & i18n

- The drawer is centred on the bottom edge and lays out logically; the × button and header padding sit on the inline end.
- Built-in string: the close label, `"Close"` or `"إغلاق"` by provider locale.

## Styling & tokens

- Surface `bg-popover` with `border-border`, `rounded-t-floating` and `shadow-floating`; handle `bg-nq-line-strong`; backdrop tinted with `nq-fg` / `nq-bg`.
- State: `data-dragging` on `[data-slot=drawer-content]`; `data-starting-style` and `data-ending-style` from Base UI drive the enter and exit.
- Extend with `className`; never use raw hex.

## Do / Don't

- **Do** keep the content short; it is capped at 85% of the viewport height and the body scrolls.
- **Do** provide a visible Cancel or × for people who do not swipe.
- **Don't** put a drag-sensitive control (a slider) on the handle strip.
- **Don't** stack drawers.

## Related

- [Sheet](https://docs.nasaqui.com/components/sheet) · [Dialog](https://docs.nasaqui.com/components/dialog) · [Popover](https://docs.nasaqui.com/components/popover)

## Lab

https://docs.nasaqui.com/?path=/docs/components-overlays-drawer--docs

## Code

### React

```tsx
import {
  Button, Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,
} from "@fadymondy/nasaq/web";

export function Example() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="secondary" />}>Open drawer</DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Edit project</DrawerTitle>
          <DrawerDescription>Drag the handle down to dismiss.</DrawerDescription>
        </DrawerHeader>
        <DrawerBody className="p-4">Content</DrawerBody>
        <DrawerFooter className="justify-end">
          <DrawerClose render={<Button variant="ghost" />}>Cancel</DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  );
}
```

### shadcn

```tsx
import { Button } from "@/components/ui/button";
import { Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer";

export function Example() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="secondary" />}>Open drawer</DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Edit project</DrawerTitle>
          <DrawerDescription>Drag the handle down to dismiss.</DrawerDescription>
        </DrawerHeader>
        <DrawerBody className="p-4">Content</DrawerBody>
        <DrawerFooter className="justify-end">
          <DrawerClose render={<Button variant="ghost" />}>Cancel</DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import {
  NqButton, NqDrawer, NqDrawerBody, NqDrawerClose, NqDrawerContent, NqDrawerDescription,
  NqDrawerFooter, NqDrawerHeader, NqDrawerTitle, NqDrawerTrigger,
} from "@fadymondy/nasaq/vue";
</script>

<template>
  <NqDrawer>
    <NqDrawerTrigger as-child><NqButton variant="secondary">Open drawer</NqButton></NqDrawerTrigger>
    <NqDrawerContent>
      <NqDrawerHeader>
        <NqDrawerTitle>Edit project</NqDrawerTitle>
        <NqDrawerDescription>Drag the handle down to dismiss.</NqDrawerDescription>
      </NqDrawerHeader>
      <NqDrawerBody class="p-4">Content</NqDrawerBody>
      <NqDrawerFooter class="justify-end">
        <NqDrawerClose as-child><NqButton variant="ghost">Cancel</NqButton></NqDrawerClose>
      </NqDrawerFooter>
    </NqDrawerContent>
  </NqDrawer>
</template>
```

### Blade

```blade
<x-nq::drawer>
    <x-nq::drawer.trigger variant="secondary">Open drawer</x-nq::drawer.trigger>
    <x-nq::drawer.content>
        <x-nq::drawer.header>
            <x-nq::drawer.title>Edit project</x-nq::drawer.title>
            <x-nq::drawer.description>Drag the handle down to dismiss.</x-nq::drawer.description>
        </x-nq::drawer.header>
        <x-nq::drawer.body class="p-4">Content</x-nq::drawer.body>
        <x-nq::drawer.footer class="justify-end">
            <x-nq::drawer.close variant="ghost">Cancel</x-nq::drawer.close>
        </x-nq::drawer.footer>
    </x-nq::drawer.content>
</x-nq::drawer>
```

### HTML + Alpine

```html
<div data-slot="drawer" x-data="nqDrawer(false)" x-modelable="open" x-id="['nq-dialog']" class="contents">
    <button data-slot="drawer-trigger"
     type="button"                         aria-haspopup="dialog" x-on:click="show()" :aria-expanded="open" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Open drawer</button>
    <template x-teleport="body">
    <div data-slot="drawer-portal">
        <div data-slot="drawer-backdrop" x-nq-presence="open" x-on:click="close()" class="fixed inset-0 z-50 bg-nq-fg/10 transition-opacity duration-200 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0 motion-reduce:transition-none dark:bg-nq-bg/60"></div>
        <div data-slot="drawer-content" x-bind="popup" x-nq-presence="open" x-trap.noscroll="open"
            :data-dragging="dragAttr" :style="shift"
            class="fixed inset-x-0 bottom-0 z-50 mx-auto flex max-h-[85dvh] w-full max-w-xl flex-col rounded-t-floating border border-b-0 border-border bg-popover text-popover-foreground shadow-floating outline-none transition-[translate,opacity] duration-200 ease-nq data-starting-style:translate-y-8 data-starting-style:opacity-0 data-ending-style:translate-y-8 data-ending-style:opacity-0 data-dragging:transition-none motion-reduce:transition-none">
                            <div data-slot="drawer-handle" aria-hidden="true" x-bind="handle" class="flex h-6 shrink-0 cursor-grab touch-none items-center justify-center active:cursor-grabbing">
                    <span class="h-1 w-10 rounded-full bg-nq-line-strong"></span>
                </div>
                        <div data-slot="drawer-header" class="flex flex-col gap-1 border-b border-border px-4 pb-3.5 pe-12"><h2 data-slot="drawer-title" :id="$id('nq-dialog', 'title')" class="text-label text-foreground">Edit project</h2>
            <p data-slot="drawer-description" :id="$id('nq-dialog', 'description')" class="text-caption text-muted-foreground">Drag the handle down to dismiss.</p></div>
        <div data-slot="drawer-body" class="min-h-0 flex-1 overflow-y-auto overscroll-contain p-4">Content</div>
        <div data-slot="drawer-footer" class="flex items-center gap-2 border-t border-border px-4 py-3 justify-end"><button data-slot="drawer-close"
     type="button"                         x-on:click="close()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Cancel</button></div>
                            <button type="button" data-slot="drawer-close" x-on:click="close()" aria-label="Close"
                    class="absolute end-3 top-3 inline-flex size-8 items-center justify-center rounded-control text-muted-foreground transition-colors duration-150 hover:bg-nq-hover hover:text-foreground focus-visible:outline-2 focus-visible:outline-nq-focus [&_svg]:size-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg></button>
                    </div>
    </div>
</template>
</div>
```
