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

# AI Mark – Code

> AiMark displays the ServiceTitan AI mark with optional animated hover states and support for tooltip or popover overlays.

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-mark-playground" fullWidth screenshot>
      ```tsx lines expandable theme={null}
      import { AiMark, Flex, Text } from "@servicetitan/anvil2";

      function App() {
        return (
          <Flex gap="2">
            <AiMark type="gradient" size="large" />
            <Text variant="headline" size="medium" el="h2">
              Created by Atlas
            </Text>
          </Flex>
        );
      }

      export default App;
      ```
    </LiveCode>

    ## Common Examples

    ```tsx theme={null}
    import { AiMark } from "@servicetitan/anvil2";

    function ExampleComponent() {
      return (
        <AiMark
          type="gradient"
          popoverOrTooltipConfig={{
            type: "tooltip",
            content: "These suggestions were written by Atlas.",
            triggerLabel: "Information about AI-powered suggestions",
          }}
        />
      );
    }
    ```

    `AiMark` displays the ServiceTitan AI mark. When paired with a tooltip or popover via `popoverOrTooltipConfig`, the icon becomes interactive and animates on hover and focus. Without a configuration, it renders as a static inline icon.

    The component automatically respects the user's `prefers-reduced-motion` system preference — animations are disabled when reduced motion is preferred.

    ### With Tooltip

    Use the tooltip variant to show a brief text label when users hover or focus the icon.

    <LiveCode showCode example="ai-mark-tooltip" fullWidth screenshot>
      ```tsx lines expandable theme={null}
      import { AiMark, Flex, Text } from "@servicetitan/anvil2";

      function App() {
        return (
          <Flex alignItems="center" gap="3">
            <Text>Call & job notes summary</Text>
            <AiMark
              type="gradient"
              popoverOrTooltipConfig={{
                type: "tooltip",
                content:
                  "Drafted from your recorded calls and past visits. Have your team verify details before the customer sees them.",
                triggerLabel: "How AI summarizes technician notes",
              }}
            />
          </Flex>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### With Popover

    Use the popover variant to show richer content — such as a title and description — when users click the icon.

    <LiveCode showCode example="ai-mark-popover" fullWidth screenshot>
      ```tsx lines expandable theme={null}
      import { AiMark, Button, Flex, Text, Textarea } from "@servicetitan/anvil2";

      function App() {
        return (
          <Flex alignItems="center" gap="3">
            <Text>Recommended estimate lines</Text>
            <AiMark
              type="gradient"
              popoverOrTooltipConfig={{
                type: "popover",
                triggerLabel: "Feedback on AI estimate suggestions",
                props: {
                  placement: "right",
                },
                content: (
                  <Flex direction="column" gap="2">
                    <Textarea
                      label="Was this suggestion helpful for your trade?"
                      rows={3}
                    />
                    <Flex gap="2" justifyContent="flex-end">
                      <Button size="small" appearance="primary" onClick={() => {}}>
                        Submit
                      </Button>
                    </Flex>
                  </Flex>
                ),
              }}
            />
          </Flex>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Force Animated

    Use `forceAnimate` to control the pulsing animation from a parent component. This is intended for cases where `AiMark` is embedded inside an interactive element and the animation should respond to the parent's hover or focus state rather than its own.

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

    function ParentComponent() {
      const [isHovered, setIsHovered] = useState(false);

      return (
        <Flex
          alignItems="center"
          gap="2"
          onMouseEnter={() => setIsHovered(true)}
          onMouseLeave={() => setIsHovered(false)}
        >
          <AiMark type="gradient" forceAnimate={isHovered} />
          <Text>AI-powered suggestions</Text>
        </Flex>
      );
    }
    ```

    ### Plain Icon

    Without `popoverOrTooltipConfig`, `AiMark` renders as a non-interactive icon. Use this when you only need the visual indicator without any overlay.

    <LiveCode showCode example="ai-mark-plain" fullWidth screenshot>
      ```tsx lines expandable theme={null}
      import { AiMark, Flex, Text } from "@servicetitan/anvil2";

      function App() {
        return (
          <Flex alignItems="center" gap="2">
            <AiMark type="gradient" />
            <Text>Created by Atlas</Text>
          </Flex>
        );
      }

      export default App;
      ```
    </LiveCode>

    ## React Accessibility

    * When `popoverOrTooltipConfig` is provided, the icon renders as a ghost `Button` with an `aria-label` set from `triggerLabel`. This ensures screen readers announce a meaningful label rather than just "button".
    * Pass a descriptive `triggerLabel` that communicates the purpose of the overlay — for example, `"Learn about AI-powered suggestions"` rather than `"AI icon"`.
    * Animations are automatically suppressed when the user has `prefers-reduced-motion: reduce` set in their system preferences.
    * The popover variant uses the internal beta `Popover` with `role="dialog"`; focus moves into the popover when it opens.
    * The popover variant calls `stopPropagation` on click to prevent unintended event bubbling when the icon is nested inside other interactive elements.
  </Tab>

  <Tab title="AiMark Props">
    ```tsx theme={null}
    <AiMark
      collapsePadding="inline"
      type="default"
      size="medium"
      popoverOrTooltipConfig={{
        type: "tooltip",
        content: "Powered by AI",
        triggerLabel: "AI feature information",
      }}
    />
    ```

    ## `AiMark` Props

    `AiMark` accepts all props from `Icon` except `svg`, plus the following:

    <ParamField path="collapsePadding" type={`"inline" | "block" | "all"`}>
      Collapses layout padding on the chosen axes so the mark’s box matches a plain icon on those axes. Interactive tooltip and popover variants keep the same hit target; non-interactive variants add padding on the non-collapsed axes.
    </ParamField>

    <ParamField path="forceAnimate" type="boolean" default="false">
      Forces the pulsing morph animation to play.
    </ParamField>

    <ParamField path="popoverOrTooltipConfig" type="AiMarkWithTooltipOrPopoverConfig">
      Configuration for an optional tooltip or popover overlay. When provided, the icon renders as an interactive button that animates on hover and focus. See `AiMarkWithTooltipOrPopoverConfig` below.
    </ParamField>

    <ParamField path="size" type={`"small" | "medium" | "large" | "xlarge"`} default="medium">
      Size of the icon.
    </ParamField>

    <ParamField path="type" type={`"default" | "gradient"`} default="default">
      Visual style of the AI mark. `"default"` inherits the current text color. `"gradient"` applies a blue gradient fill.
    </ParamField>

    ## `AiMarkWithTooltipOrPopoverConfig`

    A discriminated union — use `type` to select either the tooltip or popover variant.

    ### Tooltip variant

    <ParamField path="triggerLabel" type="string" required>
      Accessible label for the trigger button, announced to screen readers.
    </ParamField>

    <ParamField path="content" type="string">
      Text content displayed inside the tooltip. When omitted, defaults to `AI can make mistakes.`
    </ParamField>

    <ParamField path="props" type={`Omit<TooltipProps, "children">`}>
      Optional props forwarded to the `Tooltip` component.
    </ParamField>

    <ParamField path="type" type={`"tooltip"`}>
      Discriminant for the tooltip variant.
    </ParamField>

    ### Popover variant

    <ParamField path="triggerLabel" type="string" required>
      Accessible label for the trigger button, announced to screen readers.
    </ParamField>

    <ParamField path="content" type="ReactNode">
      Content rendered inside the popover. When omitted, defaults to the standard AI disclaimer message and a Learn more link to the privacy policy.
    </ParamField>

    <ParamField path="contentProps" type={`Pick<PopoverContentProps, "hideWhileClosed" | "scrollerRef">`}>
      Optional props forwarded to `Popover.Content`.
    </ParamField>

    <ParamField path="props" type={`Omit<PopoverDialogRoleProps, "children">`}>
      Optional props forwarded to the internal beta `Popover` root.
    </ParamField>

    <ParamField path="type" type={`"popover"`}>
      Discriminant for the popover variant.
    </ParamField>
  </Tab>
</Tabs>
