> ## 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 – Code

> The BottomSheet provides a resizable, bottom-anchored modal surface for general content.

export const LiveCode = ({children, customHeight, clickToLoad, example, fullWidth, fullHeight, hideCodeInLiveCode, screenshot, screenshotOnly, showCode: showCodeProp}) => {
  const SCREENSHOTS_BASE = "https://servicetitan.github.io/anvil2-docs-live-code/screenshots";
  const STACKBLITZ_BASE = "https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples";
  const [showCodeBlock, setShowCodeBlock] = useState(showCodeProp ?? false);
  const [isLocalOverride, setIsLocalOverride] = useState(false);
  useEffect(() => {
    const examplePath = `/images/live-code-screenshots-tmp/${example}.png`;
    fetch(examplePath, {
      method: "HEAD"
    }).then(r => {
      if (r.ok) setIsLocalOverride(true);
    }).catch(() => {});
  }, [example]);
  const screenshotBase = isLocalOverride ? "/images/live-code-screenshots-tmp" : SCREENSHOTS_BASE;
  if (screenshotOnly) {
    return <Frame className="flex flex-col">
        <div className="flex dark:hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#FFFFFF"
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>
        <div className="hidden dark:flex" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#141414"
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>
      </Frame>;
  }
  if (screenshot) {
    return <Frame className="flex flex-col -mb-2">
        <div className="flex dark:hidden bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>

        <div className="hidden dark:flex bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      background: "#141414",
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>

        <div className="flex justify-end items-center text-xs py-2 px-1 gap-4">
          {!showCodeProp ? <button className="inline-flex justify-end items-center text-gray-700 dark:text-gray-50 hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1 cursor-pointer" onClick={() => setShowCodeBlock(!showCodeBlock)} style={{
      appearance: "none"
    }}>
              <Icon icon="code" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
              <span>{showCodeBlock ? "Hide code" : "Show code"}</span>
            </button> : null}

          <a className="inline-flex justify-end items-center hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1" href={`${STACKBLITZ_BASE}/${example}?file=src/App.tsx`} target="_blank" rel="noreferrer">
            <Icon icon="bolt" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
            <span>StackBlitz demo</span>
          </a>
        </div>

        <div className="grid transition-[grid-template-rows] duration-300 ease-in-out overflow-auto overflow-y-hidden overflow-x-auto" style={showCodeBlock ? {
      gridTemplateRows: "1fr"
    } : {
      gridTemplateRows: "0fr"
    }}>
          <div style={{
      minHeight: 0,
      overflowX: "auto",
      overflowY: "hidden",
      marginBlockStart: "-1.25rem",
      marginBlockEnd: "-1.5rem"
    }}>
            {children}
          </div>
        </div>
      </Frame>;
  } else {
    return <div style={{
      display: "flex",
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px",
      resize: "vertical",
      overflow: "auto"
    }}>
        <iframe title={example} style={{
      flex: 1,
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px"
    }} src={`${STACKBLITZ_BASE}/${example}?embed=1&hideNavigation=1&hideExplorer=1&terminalHeight=0&file=src/App.tsx${clickToLoad ? "&ctl=1" : ""}${hideCodeInLiveCode ? "&view=preview" : ""}`} allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />
      </div>;
  }
};

<Tabs>
  <Tab title="Implementation">
    <LiveCode showCode example="ai-kit-bottom-sheet-full-page" fullWidth screenshot>
      ```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;
      ```
    </LiveCode>

    ## 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.

    <LiveCode showCode example="ai-kit-bottom-sheet-content-full-page" screenshot fullWidth>
      ```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;
      ```
    </LiveCode>

    ### Subheader

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

    <LiveCode showCode example="ai-kit-bottom-sheet-subheader-full-page" screenshot fullWidth>
      ```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;
      ```
    </LiveCode>

    ### 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.

    <LiveCode showCode example="ai-kit-bottom-sheet-close-button-full-page" screenshot fullWidth>
      ```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;
      ```
    </LiveCode>

    ### Scrolling body

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

    <LiveCode showCode example="ai-kit-bottom-sheet-scrolling-body-full-page" screenshot fullWidth>
      ```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;
      ```
    </LiveCode>

    ### 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.
  </Tab>

  <Tab title="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.

    <ParamField path="content" type="ReactNode" required>
      General body content, including forms, lists, and composed layout.
    </ParamField>

    <ParamField path="title" type="string" required>
      Accessible title rendered in the header.
    </ParamField>

    <ParamField path="className" type="string">
      Class merged onto the sheet surface.
    </ParamField>

    <ParamField path="defaultOpen" type="boolean">
      Whether the sheet starts open when uncontrolled.
    </ParamField>

    <ParamField path="isDismissable" type="boolean" default="true">
      Whether outside interaction closes the sheet.
    </ParamField>

    <ParamField path="isKeyboardDismissDisabled" type="boolean">
      Prevents Escape from closing the sheet.
    </ParamField>

    <ParamField path="isOpen" type="boolean">
      Controlled open state. Pair with `onOpenChange`.
    </ParamField>

    <ParamField path="onOpenChange" type="(isOpen: boolean) => void">
      Called when the open state changes.
    </ParamField>

    <ParamField path="showCloseButton" type="boolean" default="true">
      Shows the header close button.
    </ParamField>

    <ParamField path="subheader" type="string">
      Short secondary line beneath the title.
    </ParamField>

    <ParamField path="trigger" type="ReactElement<ButtonProps>">
      Carto `Button` that opens the sheet. Omit for controlled state.
    </ParamField>
  </Tab>
</Tabs>
