Skip to main content

Common Examples

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

{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.
Last modified on January 23, 2026