Skip to main content
Beta FeatureThis 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 channel with any questions or feedback!

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

Use the link variant when the card navigates to another page or section.
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).
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.
<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.
Last modified on January 23, 2026