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

> Interactive Card creates clickable cards that support nested interactive elements without accessibility violations.

> [!NOTE]
>
> For development, import `InteractiveCard` from
> `@servicetitan/anvil2/beta`. For design, find it in the
> [Anvil2 file](https://www.figma.com/design/xaNQsvRKczoxu7pj0M6QT8/Anvil2-v3?node-id=2264-4913).

> [!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!

![Anatomy of the Interactive Card
component](https://mintcdn.com/servicetitan/NkSrIaHzJjwUU4IF/images/docs/web/components/interactive-card/overview-image.png?fit=max\&auto=format\&n=NkSrIaHzJjwUU4IF\&q=85\&s=1a66fc85c1a813ba2b5e4736d6df056f)

## Anatomy

Interactive Card consists of three primary elements that work together to create clickable cards with nested interactive elements.

![Anatomy of the Interactive Card
component](https://mintcdn.com/servicetitan/NkSrIaHzJjwUU4IF/images/docs/web/components/interactive-card/anatomy.png?fit=max\&auto=format\&n=NkSrIaHzJjwUU4IF\&q=85\&s=b8a067c8ec70992a39433c4f0d83b7c1)

1. **Card** - Interactive card is a [Card](https://anvil.servicetitan.com/docs/components/card/design) under the hood.
2. **Card (hover)** - Interacting with the card enables clickable behavior

## Options

Interactive Card uses a Card under the hood to provide the base styling and layout. Refer to the [Card design docs](https://anvil.servicetitan.com/docs/components/card/design) for all available options.

### Simple interactive card

[interactivecard-demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/interactivecard-demo?embed=1\&hideNavigation=1\&hideExplorer=1\&terminalHeight=0\&file=src/App.tsx)

## Behavior

Interactive Card responds to user interaction with distinct visual feedback while maintaining support for nested interactive elements.

### Hover, focus, and disabled states

interactivecard-states

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/interactivecard-states?file=src/App.tsx)

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

function App() {
  return (
    <Flex gap="4">
      <InteractiveCard
        wrapperProps={{ "aria-label": "Hover state card" }}
        actionProps={{
          "aria-label": "View hover example",
          onClick: () => {},
        }}
        contentProps={{ padding: "large", flexDirection: "column", gap: "2" }}
        data-interactive="hover"
      >
        <Text>Hover state</Text>
      </InteractiveCard>

      <InteractiveCard
        wrapperProps={{ "aria-label": "Focus state card" }}
        actionProps={{
          "aria-label": "View focus example",
          onClick: () => {},
        }}
        contentProps={{ padding: "large", flexDirection: "column", gap: "2" }}
        data-interactive="focus-visible"
      >
        <Text>Focus state</Text>
      </InteractiveCard>

      <InteractiveCard
        wrapperProps={{ "aria-label": "Disabled state card" }}
        actionProps={{
          "aria-label": "View disabled example",
          onClick: () => {},
          disabled: true,
        }}
        contentProps={{ padding: "large", flexDirection: "column", gap: "2" }}
      >
        <Text>Disabled state</Text>
      </InteractiveCard>
    </Flex>
  );
}

export default App;
```

Disabled state does not apply disabled styling to nested interactive elements. Apply disabled styling manually to nested elements when needed.

### Nested interactive elements

Interactive Card can support nested interactions within the card layout itself. This can be useful when both the Card itself and nested interactive elements have different behaviors.

interactivecard-playground

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/interactivecard-playground?file=src/App.tsx)

```tsx lines 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;
```

## Usage Guidelines

| Standard | Requirement |
| - | - |
| DoClickable cards with nested interactive elements | Clickable cards with nested interactive elements. |
| DoCards that trigger actions | Cards that trigger actions (button variant). |
| DoCards that navigate | Cards that navigate to other pages (link variant). |
| DoWCAG AA 2.2 compliance | WCAG AA 2.2 compliant clickable card implementations. |
| Don'tCard without interaction | The card itself does not need an interaction. |

### Alternatives

#### Interactive Card vs Card

Use Interactive Card when the card itself needs an interaction. If no interaction is needed, or only an inner element needs an interaction, use Card instead.

#### Interactive Card vs Select Card

[Select Card](https://anvil.servicetitan.com/docs/components/select-card/web.md) provides a card-like checkbox or radio input for selection scenarios. Use Select Card when users need to select one or more options from a group of cards, such as choosing services or items. Use Interactive Card when the card triggers an action or navigates to another page, especially when the card contains nested interactive elements like buttons or links.

## Content

Content within Interactive Card should be clearly organized and include required ARIA labels for accessibility.

### Content organization

* Use a heading to make the card's purpose clear, following heading structure for the page
* Avoid too many call-to-actions in a single card
* Cards should have an intentional focus - avoid overloading a single card with too much or unrelated information
* Cards of similar importance should be structured in the same way

## Keyboard Interaction

Users can navigate Interactive Card using standard keyboard controls.

| Key | Description |
| - | - |
| Tab | Moves focus to the card or nested elements |
| Enter | Activates the card action (button variant) |
| Space | Activates the card action (button variant) |

Interactive Card provides full keyboard support for both the card action and nested interactive elements. Tab navigation moves focus between the card and its nested elements, while Enter and Space activate button variant cards.

## Code

> [!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!

**Implementation**

interactivecard-playground

[StackBlitz demo](https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples/interactivecard-playground?file=src/App.tsx)

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

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

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

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

**actionProps** `ActionButtonProps | ActionLinkProps` *required*

Props for the action element (button or link). See `ActionButtonProps` and `ActionLinkProps` below for details.

**wrapperProps** `WrapperProps` *required*

Props for the group wrapper element. See `WrapperProps` below for details.

**contentProps** `ContentProps`

Props for the content layer (Card element). Controls padding, background, gap, and other Card styling. See `ContentProps` below for details.

### `wrapperProps Object`

Props for the group wrapper element.

**aria-label** `string` *required*

ARIA label for the group wrapper describing the card's content.

> [!NOTE]
>
> The `wrapperProps` object extends all standard HTML div attributes via `PassThroughProps<"div">`, excluding `onClick` and `children`.

### `actionProps Object(s)`

#### `Button Cards (InteractiveCardWithButtonProps)`

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

**aria-label** `string` *required*

ARIA label for the button action describing what will happen when clicked.

**onClick** `(event: MouseEvent\<HTMLButtonElement\>) =\> void` *required*

Click handler for the button card.

**disabled** `boolean`

Disables the button action. Not available for link cards.

> [!NOTE]
>
> The button object extends all standard HTML button attributes via `PassThroughProps<"button">`, excluding `children`, `type`, and `href`.

#### `Link Cards (InteractiveCardWithLinkProps)`

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

**aria-label** `string` *required*

ARIA label for the link action describing where the link will navigate.

**href** `string` *required*

URL for the card to navigate to. When provided, the card renders as an anchor element.

**onClick** `(event: MouseEvent\<HTMLAnchorElement\>) =\> void`

Optional click handler for the link card. Can be used for analytics tracking alongside navigation.

> [!NOTE]
>
> The link object extends all standard HTML anchor attributes via `PassThroughProps<"a">`, excluding `children`, `disabled`, and `type`.

### `contentProps Object`

Props for the content layer (Card element). Includes all [Card component props](https://anvil.servicetitan.com/docs/components/card/react-native.md#card-props).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.