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

# Interactive Card – Code

> InteractiveCard component creates clickable cards that support nested interactive elements without accessibility violations.

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>;
  }
};

<Note>
  **Beta Feature**

  This feature is currently in beta, and needs to be imported from `@servicetitan/anvil2/beta`.

  While we hope to minimize breaking changes, they may occur due to feedback we receive or other improvements. These will always be documented in the changelog and communicated in Slack.

  Please reach out in the [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) channel with any questions or feedback!
</Note>

<Tabs>
  <Tab title="Implementation">
    <LiveCode showCode example="interactivecard-playground" fullWidth screenshot>
      ```tsx lines expandable theme={null}
      import { Button, Flex, Text } from "@servicetitan/anvil2";
      import { InteractiveCard } from "@servicetitan/anvil2/beta";

      function App() {
        return (
          <InteractiveCard
            wrapperProps={{ "aria-label": "Basic Interactive Card" }}
            actionProps={{
              "aria-label": "Select basic interactive card",
              onClick: console.log,
            }}
            contentProps={{ padding: "large", flexDirection: "column", gap: "2" }}
          >
            <Text variant="headline" el="h2" size="medium">
              Basic Interactive Card
            </Text>
            <Text>Basic text content in an interactive card.</Text>
            <Flex gap="2">
              <Button onClick={console.log}>Action </Button>
            </Flex>
          </InteractiveCard>
        );
      }

      export default App;
      ```
    </LiveCode>

    ## Common Examples

    Interactive card provides a clickable card interface that supports visually nested interactive elements without accessibility violations. Use the button variant for actions and the link variant for navigation.

    ***

    ## Button Card

    Use the button variant when the card triggers an action (e.g., expanding content, opening a modal).

    ```tsx theme={null}
    import { InteractiveCard } from "@servicetitan/anvil2/beta";
    import { Button, Flex, Text } from "@servicetitan/anvil2";

    function ExampleComponent() {
      return (
            <InteractiveCard
              wrapperProps={{ "aria-label": "Basic interactive card" }}
              actionProps={{
                "aria-label": "Select basic interactive card",
                onClick: console.log
              }}
              contentProps={{ padding: "large", flexDirection: "column", gap: "2" }}
            >
                <Text variant="headline" el="h2" size="medium">
                  Basic Interactive Card
                </Text>
                <Text>Basic text content in an interactive card.</Text>
                <Flex gap="2">
                  <Button onClick={console.log}>
                    Action
                  </Button>
                </Flex>
            </InteractiveCard>
      );
    }
    ```

    ***

    ## Link Card

    Use the link variant when the card navigates to another page or section.

    ```tsx theme={null}
    import { InteractiveCard } from "@servicetitan/anvil2/beta";
    import { Button, Flex, Text } from "@servicetitan/anvil2";

    function ExampleComponent() {
      return (
        <InteractiveCard
          wrapperProps={{ "aria-label": "[NAME]'s profile card" }}
          actionProps={{
            "aria-label": "Navigate to [NAME]'s profile",
            href: "/profile/[NAME]",
          }}
          contentProps={{ padding: "large", flexDirection: "column", gap: "3" }}
        >
            <Text variant="headline" el="h2" size="medium">
              [NAME]
            </Text>
            <Link href="[NAME]@servicetitan.com">
              [NAME]@servicetitan.com
            </Link>
            <Text>Click card to view project details</Text>
        </InteractiveCard>
      );
    }
    ```

    ***

    ## Disabled State

    Disable user interaction with the `disabled` prop on `actionProps`. Only available for button cards (not link cards).

    <Note>
      The `disabled` prop only disables the card action layer. Nested interactive elements (buttons, links, etc.) remain fully functional. If you need to disable nested elements to reflect the disabled state, handle that separately in your implementation.
    </Note>

    ```tsx theme={null}
    <InteractiveCard
      wrapperProps={{ "aria-label": "Disabled project card" }}
      actionProps={{
        "aria-label": "View project",
        onClick: () => {},
        disabled: true,
      }}
      contentProps={{ padding: "large", flexDirection: "column", gap: "3" }}
    >
        <Text variant="headline" size="medium">
          Disabled Project
        </Text>
        <Text>This card is disabled and cannot be clicked</Text>
        <Button onClick={console.log}>
          Action
        </Button>
    </InteractiveCard>
    ```

    ***

    ## React Accessibility

    * Uses a `role="group"` wrapper with `aria-label` (via `wrapperProps`) to describe the card's content
    * The interactive layer (button or link) has its own `aria-label` (via `actionProps`) describing the action
    * Full keyboard support: Tab to navigate between the card and nested elements, Enter/Space to activate
    * Touch-friendly: Handles touch events on mobile devices
    * **WCAG AA 2.2 compliant**: No nested button violations due to sibling structure approach

    ### Dual ARIA Labels

    Interactive card requires two ARIA labels to maintain accessibility:

    * **`wrapperProps["aria-label"]`**: Describes what the card contains (e.g., "Kitchen Measurement 2 card")
    * **`actionProps["aria-label"]`**: Describes the action that will be taken (e.g., "Expand Kitchen Measurement 2")

    Screen readers will announce both labels, providing context about the card's content and the available action.
  </Tab>

  <Tab title="InteractiveCard Props">
    ```tsx theme={null}
    <InteractiveCard
      wrapperProps={{ "aria-label": "Project card" }}
      actionProps={{
        "aria-label": "View project details",
        onClick: handleClick,
      }}
      contentProps={{
        padding: "medium",
        background: "strong",
      }}
    >
      Card content
    </InteractiveCard>
    ```

    ## `InteractiveCard` Props

    Interactive card uses a discriminated union type based on the `href` prop in `actionProps`: when `href` is provided, it renders as a link and `disabled` is not available. When `href` is not provided, it renders as a button and `disabled` is available.

    <ParamField path="actionProps" type="ActionButtonProps | ActionLinkProps" required>
      Props for the action element (button or link). See `ActionButtonProps` and `ActionLinkProps` below for details.
    </ParamField>

    <ParamField path="wrapperProps" type="WrapperProps" required>
      Props for the group wrapper element. See `WrapperProps` below for details.
    </ParamField>

    <ParamField path="contentProps" type="ContentProps">
      Props for the content layer (Card element). Controls padding, background, gap, and other Card styling. See `ContentProps` below for details.
    </ParamField>

    ### `wrapperProps Object`

    Props for the group wrapper element.

    <ParamField path="aria-label" type="string" required>
      ARIA label for the group wrapper describing the card's content.
    </ParamField>

    <Note>
      The `wrapperProps` object extends all standard HTML div attributes via `PassThroughProps<"div">`, excluding `onClick` and `children`.
    </Note>

    ### `actionProps Object(s)`

    #### `Button Cards (InteractiveCardWithButtonProps)`

    Props for the action element when used as a button (when `href` is not provided).

    <ParamField path="aria-label" type="string" required>
      ARIA label for the button action describing what will happen when clicked.
    </ParamField>

    <ParamField path="onClick" type="(event: MouseEvent<HTMLButtonElement>) => void" required>
      Click handler for the button card.
    </ParamField>

    <ParamField path="disabled" type="boolean">
      Disables the button action. Not available for link cards.
    </ParamField>

    <Note>
      The button object extends all standard HTML button attributes via `PassThroughProps<"button">`, excluding `children`, `type`, and `href`.
    </Note>

    #### `Link Cards (InteractiveCardWithLinkProps)`

    Props for the action element when used as a link (when `href` is provided).

    <ParamField path="aria-label" type="string" required>
      ARIA label for the link action describing where the link will navigate.
    </ParamField>

    <ParamField path="href" type="string" required>
      URL for the card to navigate to. When provided, the card renders as an anchor element.
    </ParamField>

    <ParamField path="onClick" type="(event: MouseEvent<HTMLAnchorElement>) => void">
      Optional click handler for the link card. Can be used for analytics tracking alongside navigation.
    </ParamField>

    <Note>
      The link object extends all standard HTML anchor attributes via `PassThroughProps<"a">`, excluding `children`, `disabled`, and `type`.
    </Note>

    ### `contentProps Object`

    Props for the content layer (Card element). Includes all [Card component props](/docs/web/components/card/code#card-props).
  </Tab>
</Tabs>
