Skip to main content

Common Examples

import { Button, Dialog, Flex } from "@servicetitan/anvil2";
import { useState } from "react";

function ExampleComponent() {
  const [isOpen, setIsOpen] = useState(true);
  return (
    <Dialog open={isOpen} onClose={() => setIsOpen(false)}>
      <Dialog.Header>Dialog Header</Dialog.Header>
      <Dialog.Content>This is the dialog body content.</Dialog.Content>
      <Dialog.Footer>
        <Flex gap="3" justifyContent="flex-end">
          <Dialog.CancelButton>Cancel</Dialog.CancelButton>
          <Button appearance="primary">Continue</Button>
        </Flex>
      </Dialog.Footer>
    </Dialog>
  );
}

Opening dialogs

Dialogs should be shown and hidden using the open prop, rather than conditionally rendering the entire component.This allows us to follow the HTML standard for dialog elements, which is what we render under-the-hood. This also prevents some other issues, such as skipping the exit animation or missing toast messages.
<Dialog open={shouldBeOpen} {...otherProps} />
Do
{
  shouldBeOpen && <Dialog {...otherProps} />;
}
Don’t

Rendering content on open

By default, dialog content is rendered on page load, but hidden until the dialog is opened. In some cases, such as making heavy API calls or with content that may require large re-renders based on other user actions, it could be advantageous to wait to render the dialog content.To render dialog content only when it is opened, conditionally render the children based on the state used in the Dialog.open prop.
import { Dialog, Flex, TextField } from "@servicetitan/anvil2";
import { useState } from "react";

function ExampleComponent() {
  const [isOpen, setIsOpen] = useState(true);
  return (
    <Dialog open={isOpen} onClose={() => setIsOpen(false)}>
      {isOpen && (
        <Dialog.Header>Dialog Header</Dialog.Header>
        <Dialog.Content>
          <ComponentWithHeavyAPICalls />
        </Dialog.Content>
        <Dialog.Footer>
          <Flex gap="3" justifyContent="flex-end">
            <Dialog.CancelButton>
              Cancel
            </Dialog.CancelButton>
          </Flex>
        </Dialog.Footer>
      )}
    </Dialog>
  )
}

Sticky Content

Use the sticky prop on Dialog.Content to keep important UI elements (like search fields or filters) visible while other content scrolls. This is useful for dialogs with long, scrollable content.
import { Button, Dialog, Flex, TextField, Text } from "@servicetitan/anvil2";
import { useState } from "react";

function ExampleComponent() {
  const [isOpen, setIsOpen] = useState(true);
  return (
    <Dialog open={isOpen} onClose={() => setIsOpen(false)}>
      <Dialog.Header>Filter Items</Dialog.Header>
      <Dialog.Content sticky>
        <TextField placeholder="Search items..." />
      </Dialog.Content>
      <Dialog.Content>
        <Flex direction="column" gap="2">
          {Array.from({ length: 50 }, (_, i) => (
            <Text key={i}>Item {i + 1}</Text>
          ))}
        </Flex>
      </Dialog.Content>
      <Dialog.Footer sticky>
        <Flex gap="3" justifyContent="flex-end">
          <Dialog.CancelButton>Cancel</Dialog.CancelButton>
          <Button appearance="primary">Apply</Button>
        </Flex>
      </Dialog.Footer>
    </Dialog>
  );
}
The search field will remain visible at the top while the list of items scrolls below it.

Closing dialogs

Clicking a Dialog.CancelButton or the close button inside of Dialog.Header will trigger the Dialog.onClose to run, where you can add a callback to control the open/close state of the dialog.
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
  {/* The header includes a close button that will trigger the Dialog.onClose */}
  <Dialog.Header>Header</Dialog.Header>
  <Dialog.Content>Content</Dialog.Content>
  <Dialog.Footer>
    {/* The cancel button will also trigger the Dialog.onClose */}
    <Dialog.CancelButton>Cancel</Dialog.CancelButton>
  </Dialog.Footer>
</Dialog>

Closing callbacks

In addition to onClose–which indicates a user has chosen to close a dialog–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 Dialog. For example, if you have a dialog 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 dialog.
const [open, setOpen] = useState(false);
const [field, setField] = useState("");

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

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

Dialogs and Toasts

Due to the way the HTML dialog element renders in the browser’s top layer, the Dialog 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 dialogs 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 Dialog.

{condition && <Dialog open>...</Dialog>}


<Dialog open={condition}>...</Dialog>

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 <Dialog.Content> . Since <Dialog.Content> is always present in DOM, you can add conditional to, or in, the <Dialog.Content> as well.

More details

Dialog 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 Dialog always appear above all content without needing manual z-index adjustments. Dialog internally has custom mechanism to ensure Toasts to be on top for ServiceTitan app and requires Dialog to be present on page load.
Last modified on January 23, 2026