> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Bottom Sheet - Web

> Bottom sheets present secondary content in a panel that rises from the bottom of the screen.

> [!NOTE]
>
> For development, import `BottomSheet` from
> `@servicetitan/anvil2-ai-kit`. For design, find it in the
> [AI Kit Web Component Library file](https://www.figma.com/design/QEIsArLktLeJAaFvcpw3PO/AI-Kit-Web-Component-Library).

ai-kit-bottom-sheet-full-page

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-bottom-sheet-full-page?file=src/App.tsx)

```tsx lines theme={null}
import { BottomSheet, Button } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <BottomSheet
      title="Quick actions"
      content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nSed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt."
      defaultOpen
      trigger={<Button label="Open sheet" variant="secondary" />}
    />
  );
}

export default App;
```

## Options

The Bottom Sheet supports an optional subheader, an optional close button, and any content composed from Carto components.

### Subheader

ai-kit-bottom-sheet-subheader-full-page

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-bottom-sheet-subheader-full-page?file=src/App.tsx)

```tsx lines theme={null}
import { BottomSheet, Button } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <BottomSheet
      title="Job #48213"
      subheader="Scheduled for tomorrow at 9:00 AM"
      content="Review the job details before confirming the appointment."
      defaultOpen
      trigger={<Button label="Open sheet" variant="secondary" />}
    />
  );
}

export default App;
```

An optional subheader renders a secondary line beneath the title — useful for a short status or timestamp. Omit it for a title-only header, as in the hero example above.

### Close Button

ai-kit-bottom-sheet-close-button-full-page

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-bottom-sheet-close-button-full-page?file=src/App.tsx)

```tsx lines theme={null}
import { BottomSheet, Button } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <BottomSheet
      title="Quick actions"
      content="Select an action to continue."
      showCloseButton={false}
      defaultOpen
      trigger={<Button label="Open sheet" variant="secondary" />}
    />
  );
}

export default App;
```

The header close button is shown by default. Set `showCloseButton={false}` to hide it — the sheet remains dismissable via the drag handle, the backdrop, and Escape.

### Content

ai-kit-bottom-sheet-content-full-page

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-bottom-sheet-content-full-page?file=src/App.tsx)

```tsx lines theme={null}
import { BottomSheet, Button, Checkbox } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <BottomSheet
      title="Filter jobs"
      content={
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          <Checkbox label="Show completed jobs" defaultSelected />
          <Checkbox label="Show cancelled jobs" />
          <Checkbox label="Only jobs scheduled today" />
        </div>
      }
      defaultOpen
      trigger={<Button label="Open sheet" variant="secondary" />}
    />
  );
}

export default App;
```

`content` accepts any `ReactNode`, so compose Carto components — `Checkbox`, `ArtifactCard`, `ActionLog`, and so on — to build the sheet's body.

## Behavior

The Bottom Sheet negotiates its own height from content and viewport, and its body scrolls independently of the pinned header.

### Sizing and Resizing

The sheet has a single, self-negotiated size rather than discrete size options. It opens at a start height that hugs its content up to 50% of the viewport, and the drag handle — draggable by pointer, or by Arrow Up / Arrow Down / Home / End when focused — resizes it between that start height and a 90% viewport ceiling. Dragging or pressing Arrow Down below the start height closes the sheet.

### Scrolling Body

ai-kit-bottom-sheet-scrolling-body-full-page

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-bottom-sheet-scrolling-body-full-page?file=src/App.tsx)

```tsx lines theme={null}
import { BottomSheet, Button, Text } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <BottomSheet
      title="Terms of service"
      content={
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          {Array.from({ length: 12 }, (_, i) => (
            <Text
              key={i}
              text={`Section ${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`}
            />
          ))}
        </div>
      }
      defaultOpen
      trigger={<Button label="Open sheet" variant="secondary" />}
    />
  );
}

export default App;
```

When content exceeds the current detent, the body scrolls independently while the header and drag handle stay pinned. A scroll shadow appears once the body has scrolled.

## Usage Guidelines

### When to Use

Use the Bottom Sheet when:

* **Secondary content** – Supporting content needs a modal panel that rises from the bottom of the screen.
* **Forms, lists, and composed layouts** – The body combines Carto components, such as Checkbox, Artifact Card, or Action Log.
* **Content that varies in height** – The sheet opens at the height of its content, up to half the viewport, and users drag it taller to see more.

### When not to use

Avoid using the Bottom Sheet for:

* **Short confirmations and acknowledgements** – Use a [Dialog](https://anvil.servicetitan.com/docs/components/dialog/ai-kit.md) for message-style content, such as a confirm and cancel decision or an acknowledge-only message.

## Keyboard Interaction

Users can navigate, resize, and dismiss the Bottom Sheet using standard keyboard controls.

| Key | Description |
| - | - |
| Tab | Moves focus among the drag handle, close button, and body content |
| Arrow Up / Arrow Down | Resizes the sheet by one detent when the drag handle is focused |
| Home / End | Jumps the drag handle to the start or maximum detent |
| Escape | Dismisses the sheet |

## Code

**Implementation**

ai-kit-bottom-sheet-full-page

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-bottom-sheet-full-page?file=src/App.tsx)

```tsx lines expandable theme={null}
import { BottomSheet, Button } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <BottomSheet
      title="Quick actions"
      content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nSed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt."
      defaultOpen
      trigger={<Button label="Open sheet" variant="secondary" />}
    />
  );
}

export default App;
```

## Common Examples

To use the `BottomSheet`, pass the required `title` and `content` props. Open it with a Carto `Button` through `trigger`, or control it with `isOpen` and `onOpenChange`.

```tsx theme={null}
import { BottomSheet, Button } from "@servicetitan/anvil2-ai-kit";

function ExampleComponent() {
  return (
    <BottomSheet
      title="Filters"
      trigger={<Button label="Open filters" />}
      content={<FilterForm />}
    />
  );
}
```

### Body content

Unlike `Dialog`, the sheet's `content` is a general body region for forms, lists, and composed layouts.

ai-kit-bottom-sheet-content-full-page

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-bottom-sheet-content-full-page?file=src/App.tsx)

```tsx lines expandable theme={null}
import { BottomSheet, Button, Checkbox } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <BottomSheet
      title="Filter jobs"
      content={
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          <Checkbox label="Show completed jobs" defaultSelected />
          <Checkbox label="Show cancelled jobs" />
          <Checkbox label="Only jobs scheduled today" />
        </div>
      }
      defaultOpen
      trigger={<Button label="Open sheet" variant="secondary" />}
    />
  );
}

export default App;
```

### Subheader

Pass a short `subheader` to provide context beneath the title.

ai-kit-bottom-sheet-subheader-full-page

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-bottom-sheet-subheader-full-page?file=src/App.tsx)

```tsx lines expandable theme={null}
import { BottomSheet, Button } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <BottomSheet
      title="Job #48213"
      subheader="Scheduled for tomorrow at 9:00 AM"
      content="Review the job details before confirming the appointment."
      defaultOpen
      trigger={<Button label="Open sheet" variant="secondary" />}
    />
  );
}

export default App;
```

### Close button

The header close button appears by default. Set `showCloseButton={false}` to remove it. The sheet remains dismissable through the drag handle, backdrop, and Escape.

ai-kit-bottom-sheet-close-button-full-page

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-bottom-sheet-close-button-full-page?file=src/App.tsx)

```tsx lines expandable theme={null}
import { BottomSheet, Button } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <BottomSheet
      title="Quick actions"
      content="Select an action to continue."
      showCloseButton={false}
      defaultOpen
      trigger={<Button label="Open sheet" variant="secondary" />}
    />
  );
}

export default App;
```

### Scrolling body

Content beyond the current detent scrolls inside the body while the handle and header remain visible.

ai-kit-bottom-sheet-scrolling-body-full-page

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/ai-kit-bottom-sheet-scrolling-body-full-page?file=src/App.tsx)

```tsx lines expandable theme={null}
import { BottomSheet, Button, Text } from "@servicetitan/anvil2-ai-kit";

function App() {
  return (
    <BottomSheet
      title="Terms of service"
      content={
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          {Array.from({ length: 12 }, (_, i) => (
            <Text
              key={i}
              text={`Section ${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`}
            />
          ))}
        </div>
      }
      defaultOpen
      trigger={<Button label="Open sheet" variant="secondary" />}
    />
  );
}

export default App;
```

### Controlled state

Omit `trigger` and pass `isOpen` with `onOpenChange` when an event or another component opens the sheet.

```tsx theme={null}
<>
  <Button label="Open filters" onPress={() => setOpen(true)} />
  <BottomSheet
    title="Filters"
    isOpen={isOpen}
    onOpenChange={setOpen}
    content={<FilterForm />}
  />
</>
```

## Internationalization

Localize `title`, `subheader`, and content before passing them to `BottomSheet`. The component uses these messages for built-in controls:

* `ai-kit.bottomSheet.close`
* `ai-kit.bottomSheet.resize`
* `ai-kit.bottomSheet.resizeValue`

Mount `AiKitIntlProvider` inside `CartoTheme` to switch locale or override them. `resizeValue` receives a `{percent}` value.

## React Accessibility

* The required `title` labels the modal through `aria-labelledby`. `subheader` is associated through `aria-describedby`.
* The drag handle is a vertical slider. Arrow keys move between detents and expose the current viewport percentage.
* The body is always keyboard-focusable so keyboard users can scroll plain content.
* React Aria supplies focus trapping and restoration, scroll lock, `aria-modal`, Escape dismissal, and outside-interaction dismissal.

**BottomSheet Props**

```tsx theme={null}
<BottomSheet
  title="Filters"
  subheader="Narrow your results"
  trigger={<Button label="Open filters" />}
  content={<FilterForm />}
  showCloseButton
/>
```

## `BottomSheet` Props

The `BottomSheet` accepts the following props and forwards remaining React Aria `ModalOverlay` state and dismissal props.

**content** `ReactNode` *required*

General body content, including forms, lists, and composed layout.

**title** `string` *required*

Accessible title rendered in the header.

**className** `string`

Class merged onto the sheet surface.

**defaultOpen** `boolean`

Whether the sheet starts open when uncontrolled.

**isDismissable** `boolean` (default: true)

Whether outside interaction closes the sheet.

**isKeyboardDismissDisabled** `boolean`

Prevents Escape from closing the sheet.

**isOpen** `boolean`

Controlled open state. Pair with `onOpenChange`.

**onOpenChange** `(isOpen: boolean) =\> void`

Called when the open state changes.

**showCloseButton** `boolean` (default: true)

Shows the header close button.

**subheader** `string`

Short secondary line beneath the title.

**trigger** `ReactElement\<ButtonProps\>`

Carto `Button` that opens the sheet. Omit for controlled state.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.