# Drawer

A modal panel that slides in from a screen edge, such as a bottom sheet or mobile navigation. It traps focus, locks page scrolling, and can be swiped away.

```tsx
// demo.tsx
import {Button, Drawer, Stack} from '@earendil-works/elanor-react';

export default function DrawerDemo() {
  return (
    <Drawer.Root>
      <Drawer.Trigger render={<Button hierarchy="secondary" />}>Share project</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Backdrop />
        <Drawer.Viewport>
          <Drawer.Popup>
            <Drawer.Content>
              <Stack gap="large" align="stretch">
                <Drawer.Title>Share project</Drawer.Title>
                <Drawer.Description>
                  Anyone with the link can view this project. Swipe down to dismiss.
                </Drawer.Description>
                <Stack direction="row" gap="small">
                  <Drawer.Close render={<Button />}>Copy link</Drawer.Close>
                  <Drawer.Close render={<Button hierarchy="secondary" />}>Cancel</Drawer.Close>
                </Stack>
              </Stack>
            </Drawer.Content>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}
```

## Usage

```tsx
import {Drawer} from '@earendil-works/elanor-react';

<Drawer.Root>
  <Drawer.Trigger />
  <Drawer.Portal>
    <Drawer.Backdrop />
    <Drawer.Viewport>
      <Drawer.Popup>
        <Drawer.Content>
          <Drawer.Title />
          <Drawer.Description />
          <Drawer.Close />
        </Drawer.Content>
      </Drawer.Popup>
    </Drawer.Viewport>
  </Drawer.Portal>
</Drawer.Root>;
```

Popup scrolls within the viewport. Content keeps text selectable with a mouse; add `data-base-ui-swipe-ignore` to controls that handle their own gestures.

When a bottom sheet contains form fields, wrap its Portal in `VirtualKeyboardProvider` inside Root. On touch devices, it keeps the focused field visible above the software keyboard.

## Side panel

`swipeDirection="left"` or `"right"` turns the drawer into a side panel; the default, `"down"`, is a bottom sheet. Directions are physical, so they don't flip in right-to-left layouts.

Close the drawer when the user navigates, with `actionsRef.current.close()` or controlled `open`. Don't hide an open drawer with CSS at a breakpoint; close it instead. Keep its `open` state separate from [Sidebar](/components/sidebar.md)'s `collapsed`.

```tsx
// side-panel.tsx
import {CloseIcon, MenuIcon} from '@earendil-works/elanor-icons-react';
import {
  Drawer,
  IconButton,
  SideNavigation,
  Stack,
  type DrawerRootActions,
} from '@earendil-works/elanor-react';
import {useRef, useState} from 'react';

const destinations = ['Overview', 'Projects', 'Members', 'Settings'];

export default function DrawerSidePanel() {
  const actionsRef = useRef<DrawerRootActions>(null);
  const [current, setCurrent] = useState('Overview');

  return (
    <Drawer.Root swipeDirection="left" actionsRef={actionsRef}>
      <Drawer.Trigger render={<IconButton aria-label="Open navigation" />}>
        <MenuIcon />
      </Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Backdrop />
        <Drawer.Viewport>
          <Drawer.Popup>
            <Drawer.Content>
              <Stack gap="large" align="stretch">
                <Stack direction="row" align="center" gap="auto">
                  <Drawer.Title>Acme workspace</Drawer.Title>
                  <Drawer.Close render={<IconButton aria-label="Close navigation" />}>
                    <CloseIcon />
                  </Drawer.Close>
                </Stack>
                <SideNavigation.Root aria-label="Workspace">
                  <SideNavigation.List>
                    {destinations.map((destination) => (
                      <SideNavigation.Item key={destination}>
                        <SideNavigation.Link
                          href="#"
                          active={destination === current}
                          onClick={(event) => {
                            event.preventDefault();
                            setCurrent(destination);
                            actionsRef.current?.close();
                          }}
                        >
                          {destination}
                        </SideNavigation.Link>
                      </SideNavigation.Item>
                    ))}
                  </SideNavigation.List>
                </SideNavigation.Root>
              </Stack>
            </Drawer.Content>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}
```

## Nested sheets

Nest a Drawer inside a Popup to stack bottom sheets. With `data-stacked` on each Popup, parent sheets shrink behind the open sheet. Closing a nested sheet returns focus to its trigger.

```tsx
// nested.tsx
import {Button, Drawer, Stack} from '@earendil-works/elanor-react';

export default function DrawerNested() {
  return (
    <Drawer.Root>
      <Drawer.Trigger render={<Button hierarchy="secondary" />}>Project settings</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Backdrop />
        <Drawer.Viewport>
          <Drawer.Popup data-stacked>
            <Drawer.Content>
              <Stack gap="large" align="stretch">
                <Drawer.Title>Project settings</Drawer.Title>
                <Drawer.Description>Open sharing settings in a nested sheet.</Drawer.Description>
                <Drawer.Root>
                  <Drawer.Trigger render={<Button />}>Sharing settings</Drawer.Trigger>
                  <Drawer.Portal>
                    <Drawer.Backdrop />
                    <Drawer.Viewport>
                      <Drawer.Popup data-stacked>
                        <Drawer.Content>
                          <Stack gap="large" align="stretch">
                            <Drawer.Title>Sharing settings</Drawer.Title>
                            <Drawer.Description>
                              Swipe down or close this sheet to return to project settings.
                            </Drawer.Description>
                            <Drawer.Close render={<Button />}>Back</Drawer.Close>
                          </Stack>
                        </Drawer.Content>
                      </Drawer.Popup>
                    </Drawer.Viewport>
                  </Drawer.Portal>
                </Drawer.Root>
                <Drawer.Close render={<Button hierarchy="secondary" />}>Close</Drawer.Close>
              </Stack>
            </Drawer.Content>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}
```

## Overlay placement

The application decides how overlays stack. With an elevated header, give `Drawer.Portal` `position: fixed`, `inset: 0`, and `z-index: var(--semantic-number-layer-overlay)`. Leave the backdrop, viewport, and nested popups on their default layers.

## API

### `Drawer.Root`

Accepts Base UI [`Drawer.Root`](https://base-ui.com/react/components/drawer.md) props.

### `Drawer.Trigger`

Accepts Base UI [`Drawer.Trigger`](https://base-ui.com/react/components/drawer.md) props.

### `Drawer.Portal`

Accepts Base UI [`Drawer.Portal`](https://base-ui.com/react/components/drawer.md) props.

### `Drawer.Backdrop`

Accepts Base UI [`Drawer.Backdrop`](https://base-ui.com/react/components/drawer.md) props.

### `Drawer.Viewport`

Accepts Base UI [`Drawer.Viewport`](https://base-ui.com/react/components/drawer.md) props.

### `Drawer.Popup`

Accepts Base UI [`Drawer.Popup`](https://base-ui.com/react/components/drawer.md) props.

### `Drawer.Content`

Accepts Base UI [`Drawer.Content`](https://base-ui.com/react/components/drawer.md) props.

### `Drawer.Title`

Accepts Base UI [`Drawer.Title`](https://base-ui.com/react/components/drawer.md) props.

### `Drawer.Description`

Accepts Base UI [`Drawer.Description`](https://base-ui.com/react/components/drawer.md) props.

### `Drawer.Close`

Accepts Base UI [`Drawer.Close`](https://base-ui.com/react/components/drawer.md) props.

### `Drawer.VirtualKeyboardProvider`

Accepts Base UI [`Drawer.VirtualKeyboardProvider`](https://base-ui.com/react/components/drawer.md) props.
