<!-- Protocore Design System — Tray -->
# Tray

- **Category:** Overlay (`overlay`)
- **Slug:** `overlay/tray`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { Tray } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/overlay/tray

> A bottom sheet: a panel that rises from the bottom edge for pickers, filters, and quick actions, with optional drag-to-dismiss.

## When to use it

A Tray is the **bottom-edge counterpart to a Sheet**. Where a Sheet slides in from the side for master-detail on wide screens, a Tray rises from the bottom — the natural home for **touch-first pickers, filter panels, and quick action lists**, especially on narrow viewports where a side drawer would feel cramped.

Choose a **[Sheet](/overlay/sheet)** for tall, scrollable detail views beside a list. Choose a **[Dialog](/overlay/dialog)** for a short, self-contained decision that reads better centered. Reach for a Tray when the content is a compact set of options or actions the user summons from the bottom.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `asChild` | `boolean` | no | — | — |
| `className` | `string` | no | — | — |
| `dragToDismiss` | `boolean` | no | `false` | Let the user drag the panel down past a threshold to dismiss it. |
| `grabber` | `boolean` | no | `true` | Show the top grab handle. Enabled automatically when `dragToDismiss`. |
| `showClose` | `boolean` | no | `true` | Render a top-right 32px ghost × close button. |
| `size` | `enum` | no | `md` | Panel max-height. sm 40vh / md 60vh / lg 85vh. |
| `style` | `CSSProperties` | no | — | — |

## Examples

### Basics

Tray mirrors [Sheet](/overlay/sheet) — same parts, `Tray.Root` / `Trigger` / `Content` / `Title` / `Description` / `Footer` / `Close` — but anchors to the **bottom** edge and spans the viewport width. `size` caps the height: `sm` 40vh, `md` 60vh (default), `lg` 85vh.

```tsx
import { Tray, Button, Text, VStack } from "@protocore/pds";

export default function TrayBasics() {
  return (
    <Tray.Root>
      <Tray.Trigger asChild>
        <Button variant="secondary">Open tray</Button>
      </Tray.Trigger>
      <Tray.Content>
        <Tray.Title>Filters</Tray.Title>
        <Tray.Description>Narrow the request log by status and region.</Tray.Description>
        <VStack gap={3} style={{ marginTop: 20 }}>
          <Text size="sm" color="secondary">
            Put pickers, filters, or quick actions here — the panel rises from the bottom edge.
          </Text>
        </VStack>
        <Tray.Footer>
          <Tray.Close asChild>
            <Button variant="secondary">Cancel</Button>
          </Tray.Close>
          <Tray.Close asChild>
            <Button variant="primary">Apply</Button>
          </Tray.Close>
        </Tray.Footer>
      </Tray.Content>
    </Tray.Root>
  );
}
```

### Drag to dismiss

`dragToDismiss` turns the grab handle into a drag target: pull the panel down past a third of its height to close it — the familiar mobile bottom-sheet gesture. The handle always renders (`grabber`) as an affordance; dragging is opt-in.

```tsx
import { Tray, Button, Text } from "@protocore/pds";

export default function TrayDrag() {
  return (
    <Tray.Root>
      <Tray.Trigger asChild>
        <Button variant="secondary">Open draggable tray</Button>
      </Tray.Trigger>
      <Tray.Content size="sm" dragToDismiss showClose={false}>
        <Tray.Title>Quick actions</Tray.Title>
        <Text size="sm" color="secondary" style={{ marginTop: 12 }}>
          Grab the handle and drag down to dismiss — the mobile bottom-sheet gesture.
        </Text>
      </Tray.Content>
    </Tray.Root>
  );
}
```

## Do & don't

**Do**

- Use it for pickers, filters, and quick actions — especially on touch.
- Pick a size that fits: sm for a short list, lg for a fuller panel.
- Include a Tray.Title for accessibility, as with Dialog and Sheet.
- Enable dragToDismiss for a mobile-native feel; the handle signals it.

**Don't**

- Use a Tray for long master-detail content — that's a Sheet.
- Stack multiple Trays; drive everything from one panel.
- Omit the Title — Radix warns and screen readers need the label.
- Make it taller than the content needs; a bottom sheet should feel light.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Esc` | Closes the tray and returns focus to the trigger. |
| `Tab` | Cycles focus forward, trapped within the panel. |
| `Shift + Tab` | Cycles focus backward, trapped within the panel. |
| `Enter / Space` | Activates the focused control. |

**Notes**

- Built on Radix Dialog: role=dialog with aria-modal, focus trapped and restored on close.
- A Tray.Title is required to label the panel; Description describes it.
- The drag handle is decorative (aria-hidden); dismissal is always keyboard-reachable via Esc or the close button.
- Content outside the tray is hidden from assistive tech while it's open.

## Related

`sheet`, `dialog`, `drawer`, `modals-provider`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
