# Bottom sheet

> A modal sheet that slides up from the bottom of the screen.

Source: /docs/components/bottom-sheet

```tsx
import { BottomSheet } from '@aksornproject/aksara-ui'
```

## Basic sheet

`BottomSheet` is a namespace of parts. Use `Trigger` to open it and `Close` to close it. Pass a `Button` through `render` to style either control. The popup contains the scrim, handle and sheet.

```tsx
import { BottomSheet, Button } from '@aksornproject/aksara-ui'

export default function Demo() {
  return (
    <BottomSheet.Root>
      <BottomSheet.Trigger render={<Button>Open filters</Button>} />
      <BottomSheet.Popup>
        <BottomSheet.Header title="Filters" subtitle="Choose a subject" />
        <BottomSheet.Body>Choose the filters to apply.</BottomSheet.Body>
        <BottomSheet.Footer>
          <BottomSheet.Close render={<Button size="lg">Apply</Button>} />
        </BottomSheet.Footer>
      </BottomSheet.Popup>
    </BottomSheet.Root>
  )
}
```

## Heights

`hug` follows content up to 640/844 of the overlay height. `capped` and `tall` take 523/844 and 776/844 respectively. The body scrolls when needed.

```tsx
import { BottomSheet, Button } from '@aksornproject/aksara-ui'

export default function Demo() {
  return (
    <div className="flex gap-spacing-8">
      {(['hug', 'capped', 'tall'] as const).map((height) => (
        <BottomSheet.Root key={height}>
          <BottomSheet.Trigger render={<Button>{height}</Button>} />
          <BottomSheet.Popup height={height}>
            <BottomSheet.Header title={height} />
            <BottomSheet.Body>
              Content scrolls when it exceeds the available height.
            </BottomSheet.Body>
          </BottomSheet.Popup>
        </BottomSheet.Root>
      ))}
    </div>
  )
}
```

## Without a header or footer

Use `aria-label` or a `BottomSheet.Title` to name a sheet without a header. Set `padding="flush"` for edge-to-edge content.

```tsx
import { BottomSheet, Button } from '@aksornproject/aksara-ui'

export default function Demo() {
  return (
    <BottomSheet.Root>
      <BottomSheet.Trigger render={<Button>Share</Button>} />
      <BottomSheet.Popup aria-label="Share options">
        <BottomSheet.Body padding="flush">
          <BottomSheet.Close render={<Button>Copy link</Button>} />
        </BottomSheet.Body>
      </BottomSheet.Popup>
    </BottomSheet.Root>
  )
}
```

## Non-dismissible

A non-dismissible sheet ignores dragging, scrim presses and Escape. It must be controlled with `open` and `onOpenChange`. `Close` and the header close button still close it.

```tsx
import { BottomSheet, Button } from '@aksornproject/aksara-ui'
import { useState } from 'react'

export default function Demo() {
  const [open, setOpen] = useState(false)
  return (
    <BottomSheet.Root dismissible={false} open={open} onOpenChange={setOpen}>
      <BottomSheet.Trigger render={<Button>Open required sheet</Button>} />
      <BottomSheet.Popup>
        <BottomSheet.Header title="Confirm" showClose={false} />
        <BottomSheet.Body>Use the button to close this sheet.</BottomSheet.Body>
        <BottomSheet.Footer>
          <BottomSheet.Close render={<Button size="lg">Done</Button>} />
        </BottomSheet.Footer>
      </BottomSheet.Popup>
    </BottomSheet.Root>
  )
}
```

## Container

Pass `container` to render inside a positioned element. Heights follow that element rather than the viewport.

```tsx
import { BottomSheet, Button } from '@aksornproject/aksara-ui'
import { useState } from 'react'

export default function Demo() {
  const [frame, setFrame] = useState<HTMLDivElement | null>(null)
  return (
    <div
      ref={setFrame}
      className="relative h-[500px] w-full max-w-[390px] overflow-hidden bg-color-background-body"
    >
      <BottomSheet.Root>
        <BottomSheet.Trigger render={<Button>Open in frame</Button>} />
        <BottomSheet.Popup container={frame}>
          <BottomSheet.Header title="Contained sheet" />
          <BottomSheet.Body>The sheet fills this frame.</BottomSheet.Body>
        </BottomSheet.Popup>
      </BottomSheet.Root>
    </div>
  )
}
```

## Popups inside a sheet

Select, Menu, Tooltip and nested Dialog popups portal into the sheet's host. They receive clicks and focus. Escape closes the inner popup without closing the sheet. A nested Dialog normally covers the viewport, even inside a contained sheet.

```tsx
import { BottomSheet, Button, Menu, Select } from '@aksornproject/aksara-ui'

const items = [
  { value: 'math', label: 'Math' },
  { value: 'science', label: 'Science' },
]
export default function Demo() {
  return (
    <BottomSheet.Root>
      <BottomSheet.Trigger render={<Button>Open options</Button>} />
      <BottomSheet.Popup>
        <BottomSheet.Header title="Options" />
        <BottomSheet.Body>
          <Select.Root items={items}>
            <Select.Trigger label="Subject" />
            <Select.Portal>
              <Select.Positioner>
                <Select.Popup>
                  <Select.List>
                    {items.map((item) => (
                      <Select.Item key={item.value} value={item.value}>
                        {item.label}
                      </Select.Item>
                    ))}
                  </Select.List>
                </Select.Popup>
              </Select.Positioner>
            </Select.Portal>
          </Select.Root>
          <Menu.Root>
            <Menu.Trigger>Actions</Menu.Trigger>
            <Menu.Portal>
              <Menu.Positioner>
                <Menu.Popup>
                  <Menu.Item>Save</Menu.Item>
                </Menu.Popup>
              </Menu.Positioner>
            </Menu.Portal>
          </Menu.Root>
        </BottomSheet.Body>
      </BottomSheet.Popup>
    </BottomSheet.Root>
  )
}
```

## Anatomy

```tsx
<BottomSheet.Root>
  <BottomSheet.Trigger render={<Button>Open</Button>} />
  <BottomSheet.Popup>
    <BottomSheet.Header title="Filters" />
    <BottomSheet.Body>Content</BottomSheet.Body>
    <BottomSheet.Footer>
      <BottomSheet.Close render={<Button>Done</Button>} />
    </BottomSheet.Footer>
  </BottomSheet.Popup>
</BottomSheet.Root>
```

The footer stacks children by default and preserves their JSX order. Use `className` to change the layout.

## Accessibility

- The header title names the sheet and its subtitle describes it. Without a header, provide a Title or an accessible label.
- Focus moves to the popup on open and returns to the trigger on close. Pass a ref or function to `initialFocus` to focus another element.
- The header close button is named "Close" by default. Pass `closeLabel` to translate it.
- The sheet is modal. Use a non-dismissible sheet only when the task needs an explicit action.

## API

### BottomSheet.Root

| Prop | Type | Description |
| --- | --- | --- |
| `dismissible` | `boolean` | Drag down, a scrim press and Escape close the sheet. Defaults to `true`. Drag down, a scrim press and Escape do nothing. `Close` and the Header close button still call `onOpenChange(false)`, so the sheet must be controlled. |
| `open` | `boolean` | The controlled open state. |
| `defaultOpen` | `boolean` | The open state on first render when uncontrolled. |
| `onOpenChange` | `((open: boolean) => void) \| ((open: boolean) => void)` | Runs when the sheet asks to open or close. Runs when `Close` or the Header close button asks to close. |
| `onOpenChangeComplete` | `((open: boolean) => void)` | Runs after the open or close animation, with the new open state. |
| `children` | `ReactNode` | The Trigger and the Popup. |

### BottomSheet.Trigger

| Prop | Type | Description |
| --- | --- | --- |
| `render` | `ReactElement<{ children?: ReactNode; }, string \| React.JSXElementConstructor<any>>` | The element that becomes the trigger. Its props merge with the trigger props. |

### BottomSheet.Close

| Prop | Type | Description |
| --- | --- | --- |
| `render` | `ReactElement<{ children?: ReactNode; }, string \| React.JSXElementConstructor<any>>` | The element that becomes the close control. Its props merge with the close props. |

### BottomSheet.Popup

| Prop | Type | Description |
| --- | --- | --- |
| `height` | `"capped" \| "hug" \| "tall"` | Defaults to `'hug'`. |
| `container` | `DocumentFragment \| Element \| null` | Renders the scrim and the sheet inside this element instead of `<body>`. They fill the element and `height` follows its height. Give the element a non-static `position`. Sheets nested in a contained sheet stay inside the same element. |
| `initialFocus` | `RefObject<HTMLElement \| null> \| (() => HTMLElement \| null)` | The element that takes focus on open. Defaults to the sheet surface. |
| `className` | `string` | Classes merged last on the sheet surface. |
| `children` | `ReactNode` | The Header, Body and Footer. Without a Header, pass `aria-label`. |

### BottomSheet.Header

| Prop | Type | Description |
| --- | --- | --- |
| `title` (required) | `ReactNode` | Rendered in a Title, so it names the sheet. Clamps to two lines. |
| `subtitle` | `ReactNode` | Rendered in a Description, so it describes the sheet. |
| `className` | `string` | Classes merged last on the header. |
| `showClose` | `boolean` | Shows the close button. Defaults to `true`. |
| `closeLabel` | `string` | Accessible name of the close button. Defaults to `'Close'`. |

### BottomSheet.Title

| Prop | Type | Description |
| --- | --- | --- |
| `className` | `string` | Classes merged last on the title. |

### BottomSheet.Description

| Prop | Type | Description |
| --- | --- | --- |
| `className` | `string` | Classes merged last on the description. |

### BottomSheet.Body

| Prop | Type | Description |
| --- | --- | --- |
| `padding` | `"default" \| "flush"` | Defaults to `'default'`. |

### BottomSheet.Footer

`BottomSheetFooterProps` accepts div props and a forwarded ref. Its `className` merges last.