---
title: ExpandableCard
description: An interactive card component that expands and collapses to reveal additional content, with optional image background, tags, and smooth animations
---

```tsx
'use client';

import { ExpandableCard } from '@/ui/cards/ExpandableCard';

export function ExpandableCardDemo() {
  return (
    <ExpandableCard title="Expandable Card Title" subtitle="Click to expand and see more details">
      <div className="p-20">
        <p className="text-grey">
          This is the expandable content that appears when you click the card. It smoothly animates
          in and out using the ElasticHeight component.
        </p>
      </div>
    </ExpandableCard>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/expandable-card.json
```

Or manually, copy the following code into `/ui/expandable-card.tsx`:

```tsx
'use client';

import clsx from 'clsx';
import { type ComponentProps, type FunctionComponent, useState } from 'react';

import { Clickable } from '../Clickable';
import { Heading } from '../heading/Heading';
import { Tag } from '../Tag';
import { CardBackground, type CardBackgroundProps } from './CardBackground';
import { ElasticHeight } from '../ElasticHeight';
import { Card, type CardProps } from './Card';
import { HeadingGroup } from '../heading/HeadingGroup';
import { Button } from '../buttons/Button';

export interface ExpandableCardProps extends Omit<CardProps<'article'>, 'onClick'> {
  hLevel?: ComponentProps<typeof Heading>['level'];
  image?: CardBackgroundProps;
  onClick?: (isExpanded: boolean) => void;
  subtitle?: string | null;
  tags?: ComponentProps<typeof Tag>[] | null;
  title: string;
}

export const ExpandableCard: FunctionComponent<ExpandableCardProps> = ({
  children,
  className,
  hLevel = 3,
  image,
  onClick,
  subtitle,
  tags,
  title,
  component = 'article',
  ...attrs
}) => {
  const hasTitle = Boolean(title);

  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <Card<'article'>
      component={component}
      data-name="ExpandableCard"
      data-testid="expandable-card"
      {...attrs}
      className={clsx({ 'border-t-0': image?.src }, className)}
      data-open={isExpanded}
    >
      <Clickable
        className="group relative -m-1 block aspect-horizontal overflow-hidden rounded-16 text-start"
        data-testid="expandable-card-clickable"
        onClick={() => {
          onClick?.(!isExpanded);
          setIsExpanded(!isExpanded);
        }}
      >
        <CardBackground {...(image || {})}>
          {Boolean(tags?.length) && (
            <div className="flex flex-wrap gap-4 self-start p-20">
              {tags?.map((tag) => <Tag {...tag} key={tag.label} />)}
            </div>
          )}
          <div
            className={clsx('mt-auto flex justify-between gap-x-8 p-20', {
              'text-white': image?.src,
            })}
          >
            {hasTitle && (
              <HeadingGroup subtitle={subtitle} level={hLevel}>
                {title}
              </HeadingGroup>
            )}
            <Button
              component="span"
              className="!transition duration-500"
              color={image?.src ? 'white' : 'black'}
              icon="ArrowDefaultDown"
              style={{ rotate: isExpanded ? '-180deg' : '0deg' }}
              tabIndex={-1}
              theme="outline"
              variant="circle"
            />
          </div>
        </CardBackground>
      </Clickable>
      <ElasticHeight isExpanded={isExpanded}>{children}</ElasticHeight>
    </Card>
  );
};
```

## Usage

```tsx showLineNumbers
import { ExpandableCard } from '@/docs/lib/ui/expandable-card';

export function Component() {
  return (
    <ExpandableCard
      title="Explore the Mediterranean"
      subtitle="Discover amazing destinations"
      image={{
        src: '/images/destination.jpg',
        alt: 'Mediterranean coast',
      }}
      tags={[
        {
          label: 'All Inclusive',
          backgroundColor: 'saffron',
          color: 'black',
        },
      ]}
      onClick={(isExpanded) => {
        console.log('Card expanded:', isExpanded);
      }}
    >
      <div className="p-20">
        <p>
          Experience pristine beaches, crystal-clear waters, and world-class amenities at our
          Mediterranean resorts.
        </p>
      </div>
    </ExpandableCard>
  );
}
```

## API Reference

### ExpandableCard

| Prop        | Type                                   | Default     | Description                                                                                                                       |
| ----------- | -------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `title`     | `string`                               | `undefined` | **Required**. The main heading text displayed in the card.                                                                        |
| `subtitle`  | `string \| null`                       | `undefined` | Optional subtitle text displayed below the title.                                                                                 |
| `hLevel`    | `1 \| 2 \| 3 \| 4 \| 5 \| 6`           | `3`         | The semantic heading level for the title (h1-h6).                                                                                 |
| `image`     | `CardBackgroundProps`                  | `undefined` | Optional background image configuration. When provided, renders an image with a gradient overlay and changes text color to white. |
| `tags`      | `ComponentProps<typeof Tag>[] \| null` | `undefined` | Optional array of tag configurations to display at the top of the card.                                                           |
| `onClick`   | `(isExpanded: boolean) => void`        | `undefined` | Optional callback function that fires when the card is clicked. Receives the new expanded state as a parameter.                   |
| `component` | `string`                               | `"article"` | The HTML element type to render as the root element.                                                                              |
| `children`  | `ReactNode`                            | `undefined` | The collapsible content that appears when the card is expanded. Smoothly animates using ElasticHeight.                            |
| `className` | `string`                               | `undefined` | Additional CSS classes to apply to the root card element.                                                                         |

The component also accepts all standard HTML element attributes based on the `component` prop (defaults to `article` attributes) via `CardProps`.

## Notes

- **Interactive Behavior**: The card automatically manages its own expanded/collapsed state internally using React's `useState` hook.
- **Smooth Animations**: Uses the `ElasticHeight` component to provide smooth height transitions when expanding and collapsing content.
- **Keyboard Accessible**: The clickable area uses the `Clickable` component, making it fully keyboard accessible with proper ARIA attributes.
- **Visual Indicator**: A circular button with an arrow icon rotates 180 degrees to indicate the expanded/collapsed state.
- **Image Support**: When an image is provided, the card displays it as a background with a gradient overlay and automatically changes text to white for better contrast.
- **Tag Display**: Tags are rendered at the top of the card in a flex-wrap container, allowing multiple tags to flow naturally.
- **Semantic HTML**: By default renders as an `<article>` element, but can be customized via the `component` prop for different semantic contexts.
- **Card Styling**: Inherits all styling features from the base `Card` component, including themes (bordered, frame, none) and formats (horizontal, vertical, square, custom).
- **State Callback**: The `onClick` callback receives the new expanded state (true/false), allowing parent components to react to state changes.
- **Border Behavior**: When an image is present, the top border is automatically removed (`border-t-0`) for a seamless visual appearance.
- **Data Attributes**: The component sets `data-name="ExpandableCard"` and `data-open={isExpanded}` attributes for styling and testing purposes.
