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

# Drawer – Code

> Drawers are flyouts that slide onto the page. All drawers slide in from the right.

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="drawer-playground-full-page" fullWidth screenshot>
      ```tsx lines expandable theme={null}
      import { Drawer, Button } from "@servicetitan/anvil2";

      function App() {
        return (
          <Drawer open>
            <Drawer.Header>Header text</Drawer.Header>
            <Drawer.Content>Body text</Drawer.Content>
            <Drawer.Footer>
              <Drawer.CancelButton>Cancel</Drawer.CancelButton>
              <Button appearance="primary">Footer Button</Button>
            </Drawer.Footer>
          </Drawer>
        );
      }

      export default App;
      ```
    </LiveCode>

    ## Common Examples

    ```tsx theme={null}
    import { Button, Drawer } from "@servicetitan/anvil2";

    function ExampleComponent() {
      return (
        <Drawer open>
          <Drawer.Header>Header text</Drawer.Header>
          <Drawer.Content>Body text</Drawer.Content>
          <Drawer.Footer>
            <Button appearance="primary">Footer Button</Button>
          </Drawer.Footer>
        </Drawer>
      );
    }
    ```

    ### Closing Drawers

    Clicking a `Drawer.CancelButton` or the close button in the `Drawer.Header` will trigger the `onClose` handler. A callback can be added to a `Drawer.CancelButton` or any `Button` in the `Drawer.Footer` to further control the open state of the `Drawer`.

    #### Closing callbacks

    In addition to `onClose`–which indicates a user has chosen to close a drawer–the component also offers two animation callbacks `onCloseAnimationStart` and `onCloseAnimationComplete`.

    You may use these callbacks to perform additional actions related to the presentation of the `Drawer`. For example, if you have a drawer containing a form, you may wish to reset the form state after the close animation has played so that the user doesn't see an empty form briefly when closing the drawer.

    ```tsx theme={null}
    const [open, setOpen] = useState(false);
    const [field, setField] = useState("");

    const handleSubmit = () => {
      setOpen(false);
    };
    const handleDrawerAnimationComplete = () => {
      setField("");
    };

    return (
      <>
        <Button onClick={() => setOpen(true)}>Open</Button>
        <Drawer
          open={open}
          onClose={() => setOpen(false)}
          onCloseAnimationComplete={handleDrawerAnimationComplete}
        >
          <Drawer.Header>What is your favorite color?</Drawer.Header>
          <Drawer.Content>
            <TextField
              label="Favorite color"
              value={field}
              onChange={(e) => setField(e.target.value)}
              style={{ width: "100%" }}
              autoComplete="off"
            />
          </Drawer.Content>
          <Drawer.Footer>
            <Button onClick={() => setOpen(false)}>Cancel</Button>
            <Button appearance="primary" onClick={handleSubmit}>
              Submit
            </Button>
          </Drawer.Footer>
        </Drawer>
      </>
    );
    ```

    ### Sticky Content

    Use the `sticky` prop on `Drawer.Content` to keep important UI elements (like search fields or filters) visible while other content scrolls. This is useful for drawers with long, scrollable content.

    ```tsx theme={null}
    import { Button, Drawer, Flex, TextField, Text } from "@servicetitan/anvil2";
    import { useState } from "react";

    function ExampleComponent() {
      const [isOpen, setIsOpen] = useState(true);
      return (
        <Drawer open={isOpen} onClose={() => setIsOpen(false)}>
          <Drawer.Header>Filter Items</Drawer.Header>
          <Drawer.Content sticky>
            <TextField placeholder="Search items..." />
          </Drawer.Content>
          <Drawer.Content>
            <Flex direction="column" gap="2">
              {Array.from({ length: 50 }, (_, i) => (
                <Text key={i}>Item {i + 1}</Text>
              ))}
            </Flex>
          </Drawer.Content>
          <Drawer.Footer sticky>
            <Flex justifyContent="space-between" grow="1">
              <Button>Reset</Button>
              <Flex gap="3">
                <Drawer.CancelButton>Cancel</Drawer.CancelButton>
                <Button appearance="primary">Apply</Button>
              </Flex>
            </Flex>
          </Drawer.Footer>
        </Drawer>
      );
    }
    ```

    The search field will remain visible at the top while the list of items scrolls below it.

    ### Drawers and Toasts

    Due to the way the HTML `dialog` element renders in the browser's top layer, the `Drawer` component includes an internal `Toaster` for rendering toast messages. This should be unnoticeable to implementors and users, but there may be edge cases the result in toasts not rendering as expected.

    Please reach out to us in the [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) channel on Slack if any edge cases related to drawers and toasts are found!

    ## Anti-Patterns

    ### Conditional rendering

    The openness should be controlled by the `open` prop and not by conditional rendering -- this is an anti-pattern for Drawer.

    ```tsx theme={null}
    ❌
    {condition && <Drawer open>...</Drawer>}

    ✅
    <Drawer open={condition}>...</Drawer>
    ```

    #### Resetting content

    HTML Dialog show/hide content but it doesn't remove from the DOM which means the reset doesn't happen automatically. To do the reset, use `key` on `<Drawer.Content>` . Since `<Drawer.Content>` is always present in DOM, you can add conditional to, or in, the `<Drawer.Content>` as well.

    #### More details

    Drawer uses HTML Dialog which is powered by HTML top-layers. This puts them on top of EVERYTHING regardless z-index and we use the HTML top-layer to avoid stacking context and z-index issues, ensuring Drawer always appear above all content without needing manual z-index adjustments. Drawer internally has custom mechanism to ensure Toasts to be on top for ServiceTitan app and requires Drawer to be present on page load.
  </Tab>

  <Tab title="Drawer Props">
    ```tsx theme={null}
    <Drawer
      open={true}
      onClose={() => setIsOpen(false)}
      disableCloseOnClickOutside={false}
      disableCloseOnEscape={false}
      size="medium"
      onCloseAnimationComplete={() => {}}
      onCloseAnimationStart={() => {}}
      onOpenAnimationComplete={() => {}}
      onOpenAnimationStart={() => {}}
    >
      <Drawer.Header>Header</Drawer.Header>
      <Drawer.Content>Content</Drawer.Content>
      <Drawer.Footer>
        <Button>Action</Button>
      </Drawer.Footer>
    </Drawer>
    ```

    ## `Drawer` Props

    In addition to the props listed below, the `Drawer` component can accept any valid HTML `dialog` props.

    <ParamField path="disableCloseOnClickOutside" type="boolean" default="false">
      When `true`, clicking outside the drawer will not close it.
    </ParamField>

    <ParamField path="disableCloseOnEscape" default="false" type="boolean">
      When `true`, pressing the escape key will not close the drawer.
    </ParamField>

    <ParamField path="enableScrollChaining" type="boolean">
      Enables scroll chaining behavior.
    </ParamField>

    <ParamField path="initialFocusResolver" type="(focusables: (HTMLElement | SVGElement)[]) => (HTMLElement | SVGElement)">
      Given an array of focusable elements, returns the element that should receive initial focus.
    </ParamField>

    <ParamField path="onClickOutside" type="(e: MouseEvent) => void">
      Callback when clicking outside the drawer.
    </ParamField>

    <ParamField path="onClose" type="() => void">
      Indicates the user has closed the drawer. Use this to update your state.
    </ParamField>

    <ParamField path="onCloseAnimationComplete" type="() => void">
      Callback indicating the drawer has animated out. Use this to clean up views as needed.
    </ParamField>

    <ParamField path="onCloseAnimationStart" type="() => void">
      Callback indicating the drawer is beginning to animate out.
    </ParamField>

    <ParamField path="onOpenAnimationComplete" type="() => void">
      Callback indicating the drawer has animated in.
    </ParamField>

    <ParamField path="onOpenAnimationStart" type="() => void">
      Callback indicating the drawer is beginning to animate in.
    </ParamField>

    <ParamField path="open" default="false" type="boolean" />

    <ParamField path="size" type={`"small" | "medium" | "large" | "xlarge"`} default="medium" />
  </Tab>

  <Tab title="Drawer.Header Props">
    ```tsx theme={null}
    <Drawer.Header>Drawer Header</Drawer.Header>
    ```

    ## `Drawer.Header` Props

    The `Drawer.Header` component can accept any valid HTML `header` props.
  </Tab>

  <Tab title="Drawer.Content Props">
    ```tsx theme={null}
    <Drawer.Content sticky>Drawer content</Drawer.Content>
    ```

    ## `Drawer.Content` Props

    In addition to the props listed below, the `Drawer.Content` component can accept any valid HTML `div` props.

    <ParamField path="sticky" type="boolean" default="false">
      When `true`, the content will stick below the header during scroll.
    </ParamField>
  </Tab>

  <Tab title="Drawer.Footer Props">
    ```tsx theme={null}
    <Drawer.Footer sticky={false}>
      <Button>Action</Button>
    </Drawer.Footer>
    ```

    ## `Drawer.Footer` Props

    In addition to the props listed below, the `Drawer.Footer` component can accept any valid HTML `footer` props.

    <ParamField path="sticky" default="false" type="boolean" />
  </Tab>
</Tabs>
