# Drawer

> A family of composable components for edge-sliding panels with remote triggers, keyboard navigation, focus trapping, and full ARIA support.

# Drawer

## Overview

Use the Drawer family when you need a panel that slides in from the edge of the viewport — for mobile navigation, filter panels, quick settings, or any overlay content that should feel spatially connected to the screen edge. The family is composed of five components: a root panel, a remote trigger, a title, a description, and a close button. The trigger lives separately from the drawer — they connect by matching IDs, not by DOM nesting.

## Authoring Structure

**Important:** `DrawerTrigger` is always placed separately from `Drawer` — they are siblings, not nested. They connect via matching IDs: `identity.drawerId` on the Drawer must match `targeting.targetDrawerId` on the Trigger.

```
DrawerTrigger (separate, anywhere on page)
Drawer (the sliding panel)
├── DrawerTitle
├── DrawerDescription (optional)
├── (your content)
└── DrawerClose
```

### Placement Rules

| Component | Placement | Role |
|---|---|---|
| **DrawerTrigger** | Separate from `Drawer`. Anywhere on the page. | Button that opens the drawer by targeting its ID. |
| **Drawer** | Top-level. Can be anywhere in the DOM. | The sliding panel surface. Hosts title, description, content, and close. |
| **DrawerTitle** | Inside `Drawer`. | Provides accessible name via `aria-labelledby`. Renders as a heading. |
| **DrawerDescription** | Inside `Drawer`. Optional. | Provides accessible description via `aria-describedby`. |
| **DrawerClose** | Inside `Drawer`. | Button that dismisses the drawer. |

## Quick Start

<CodeExample title="Basic — bottom drawer with close button" language="jsx">
{`<OmeDrawerTrigger
  targeting='{{"targetDrawerId":"mobile-menu"}}'
  content='{{"label":"Menu"}}'
/>

<OmeDrawer
  identity='{{"drawerId":"mobile-menu"}}'
  settings='{{"direction":"bottom","dismissible":true}}'
>
  {#slot default}
    <OmeDrawerTitle>Navigation</OmeDrawerTitle>
    <nav>
      Home
      About
      Contact
    </nav>
    <OmeDrawerClose>Close</OmeDrawerClose>
  {/slot}
</OmeDrawer>`}
</CodeExample>

The trigger and drawer are siblings connected by matching IDs (`mobile-menu`). The trigger can be placed anywhere on the page — it does not need to be near the drawer.

<CodeExample title="Right-side filter panel with custom styling" language="jsx">
{`<OmeDrawerTrigger
  targeting='{{"targetDrawerId":"filter-panel"}}'
  styling='{{"class":"btn btn--outline"}}'
>
  {#slot default}Filters{/slot}
</OmeDrawerTrigger>

<OmeDrawer
  identity='{{"drawerId":"filter-panel"}}'
  settings='{{"direction":"right","dismissible":true}}'
  styling='{{"class":"ome-drawer-default filter-panel"}}'
>
  {#slot default}
    <OmeDrawerTitle structure='{{"tag":"h3"}}'>Filters</OmeDrawerTitle>
    <OmeDrawerDescription>Refine your search results.</OmeDrawerDescription>
    
      <!-- filter content -->
    
    <OmeDrawerClose styling='{{"class":"btn btn--icon"}}'>
      {#slot default}&times;{/slot}
    </OmeDrawerClose>
  {/slot}
</OmeDrawer>`}
</CodeExample>

Use custom slot content when you need icons, badges, or any markup beyond a plain label in the trigger or close button.

---

## Family Components

### Drawer (Root)

The root component renders the sliding panel surface. It is targeted by one or more `DrawerTrigger` components via a shared ID. When opened, the drawer's DOM node is physically moved into a shared runtime host — it does not render at its authored position. On close, the node is restored to its original location using a Comment anchor.

<PropsTable group="Identity">
  <Prop name="drawerId" type="string" defaultValue='""'>
    Unique ID that triggers use to target this drawer. Must match `targetDrawerId` on any trigger that should open it.
  </Prop>
</PropsTable>

<PropsTable group="Settings">
  <Prop name="direction" type={`"bottom" | "right" | "top" | "left"`} defaultValue="bottom">
    Edge the drawer slides from. `"bottom"` creates a bottom sheet with a drag handle. Other directions slide in without a handle.
  </Prop>
  <Prop name="defaultOpen" type="boolean" defaultValue="false">
    Opens the drawer automatically when the runtime initializes.
  </Prop>
  <Prop name="dismissible" type="boolean" defaultValue="true">
    When `true`, clicking the overlay or pressing `Escape` closes the drawer. When `false`, the drawer can only be closed via `DrawerClose`.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-drawer-default">
    CSS class applied to the drawer surface element.
  </Prop>
</PropsTable>

#### Direction Behavior

| Direction | Behavior |
|---|---|
| **`bottom`** | Slides up from the bottom edge. A drag handle element is auto-generated for bottom-sheet interaction. |
| **`right`** | Slides in from the right edge. No handle element. |
| **`top`** | Slides down from the top edge. No handle element. |
| **`left`** | Slides in from the left edge. No handle element. |

#### CSS Custom Properties

| Property | Default | Description |
|---|---|---|
| `--ome-drawer-overlay-color` | `rgba(0, 0, 0, 0.5)` | Background color of the overlay behind the drawer. |
| `--ome-drawer-animation-duration` | `250ms` | Duration of the slide in/out transition. |

#### Key Behaviors

- **One active drawer at a time** — opening a drawer closes any currently open drawer.
- **DOM portaling** — the drawer node is moved into a shared runtime host while open and restored on close.
- **Focus trap** — Tab/Shift+Tab is trapped inside the drawer while open.
- **Focus restoration** — focus returns to the opener element when the drawer closes.
- **Scroll lock** — body scrolling is disabled while the drawer is open, with `scrollbar-gutter: stable` to prevent layout shift.

#### Accessibility

- Drawer receives `role="dialog"` and `aria-modal="true"`.
- `aria-labelledby` is wired to the `DrawerTitle` element's ID.
- `aria-describedby` is wired to the `DrawerDescription` element's ID (if present).
- The drag handle (bottom direction only) gets `aria-hidden="true"`.
- IDs are auto-generated if not explicitly set.

---

### DrawerTrigger

The trigger is a `<button>` that opens a specific drawer by targeting its ID. It is always placed separately from the `Drawer` — they are siblings in the editor, not nested. Multiple triggers can target the same drawer by sharing the same `targetDrawerId`.

<PropsTable group="Targeting">
  <Prop name="targetDrawerId" type="string" defaultValue='""'>
    ID of the drawer this trigger should open. Must match `identity.drawerId` on the target `Drawer`.
  </Prop>
</PropsTable>

<PropsTable group="Content">
  <Prop name="label" type="string" defaultValue='""'>
    Fallback label text. Used when the trigger slot is empty to render a plain text label inside the button.
  </Prop>
</PropsTable>

<PropsTable group="Settings">
  <Prop name="disabled" type="boolean" defaultValue="false">
    Disables the trigger so it cannot open the drawer.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-drawer-trigger-default">
    CSS class applied to the trigger button element.
  </Prop>
</PropsTable>

#### Trigger Accessibility

- `aria-haspopup="dialog"` on the trigger button.
- `aria-expanded` reflects whether the targeted drawer is open.
- `aria-controls` points to the drawer element's ID.
- `Enter` and `Space` open the targeted drawer.

---

### DrawerTitle

Provides an accessible name for the drawer via heading semantics. The drawer wires `aria-labelledby` to this element's ID automatically.

<PropsTable group="Structure">
  <Prop name="tag" type="string" defaultValue="h2">
    HTML tag for the title element. Use an appropriate heading level for your document outline.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-drawer-title-default">
    CSS class applied to the title element.
  </Prop>
</PropsTable>

---

### DrawerDescription

Provides an accessible description for the drawer. The drawer wires `aria-describedby` to this element's ID automatically. Optional — omit it if the drawer does not need a description.

<PropsTable group="Structure">
  <Prop name="tag" type="string" defaultValue="p">
    HTML tag for the description element.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-drawer-description-default">
    CSS class applied to the description element.
  </Prop>
</PropsTable>

---

### DrawerClose

A `<button>` that closes the active drawer. Place it inside the `Drawer` — typically at the top or bottom of the drawer content.

<PropsTable group="Settings">
  <Prop name="disabled" type="boolean" defaultValue="false">
    Disables the close button.
  </Prop>
</PropsTable>

<PropsTable group="Styling">
  <Prop name="class" type="class" defaultValue="ome-drawer-close-default">
    CSS class applied to the close button element.
  </Prop>
</PropsTable>

---

## Keyboard Behavior

| Key | Context | Action |
|---|---|---|
| `Enter` / `Space` | Trigger focused | Opens the targeted drawer. |
| `Escape` | Drawer open | Closes the drawer (only when `dismissible="true"`). |
| `Tab` | Drawer open | Moves focus to the next focusable element inside the drawer (trapped). |
| `Shift` + `Tab` | Drawer open | Moves focus to the previous focusable element inside the drawer (trapped). |
| `Enter` / `Space` | Close button focused | Closes the drawer. |

---

## Common Mistakes

<CommonMistake title="Nesting DrawerTrigger inside Drawer">
  `DrawerTrigger` and `Drawer` must be **siblings**, not nested. The trigger targets the drawer by matching IDs (`targetDrawerId` = `drawerId`), not by DOM relationship. Nesting the trigger inside the drawer breaks the connection.
</CommonMistake>

<CommonMistake title="Forgetting to set drawerId and targetDrawerId">
  The trigger-to-drawer connection relies entirely on matching IDs. If `identity.drawerId` on the `Drawer` does not match `targeting.targetDrawerId` on the `DrawerTrigger`, the trigger will not open the drawer.
</CommonMistake>

<CommonMistake title="Expecting the drawer to appear at its authored position">
  When a drawer opens, its DOM node is physically moved into a shared runtime host. It does not render where you placed it in the editor. On close, it is restored to its original position.
</CommonMistake>

<CommonMistake title="Confused by the bottom direction handle element">
  When `direction="bottom"`, a drag handle element is auto-generated inside the drawer for bottom-sheet interaction. This handle has `aria-hidden="true"` and is not interactive via keyboard — it is a visual and touch affordance only.
</CommonMistake>

---

## FAQs

<details>
<summary>Can multiple triggers open the same drawer?</summary>

Yes. Set the same `targetDrawerId` on all triggers that should open the drawer. Each trigger independently targets the drawer by ID, so any number of triggers can point to the same drawer.

</details>

<details>
<summary>What is the difference between Dialog and Drawer?</summary>

Dialog is a centered modal surface with 9 placement options. Drawer slides in from a screen edge with 4 directions (bottom, right, top, left). Both trap focus and share the same one-active-at-a-time constraint — opening a drawer closes any open dialog, and vice versa.

</details>

<details>
<summary>Why does only the bottom direction have a handle?</summary>

The handle is a drag affordance specific to bottom-sheet interaction patterns. Users expect to be able to drag a bottom sheet down to dismiss it. Side and top drawers do not use this pattern, so no handle is generated for those directions.

</details>

<details>
<summary>Does the drawer animate?</summary>

Yes. The drawer uses a CSS transform transition for the slide-in/out animation. The duration is configurable via the `--ome-drawer-animation-duration` custom property (default: `250ms`).

</details>

<details>
<summary>Can I have a drawer inside a dialog?</summary>

Yes, but only one can be active at a time across both families. Opening a drawer will close any currently open dialog, and opening a dialog will close any currently open drawer.

</details>
