# Card

**When to use:** Surface container for grouping related content — list items, profile cards, dashboard tiles, product listings.

**Keywords:** panel, box

**Import:** `import { Card, CardStyleType, CardHeader, CardMedia, CardContent, CardActions, ThemeColor } from '@ahrowe/ui'`

**Styles:** `Outlined` and `Light` mean what they mean on Button, Badge and Chip; `color` is the
colour they use (a `ThemeColor`, default `Neutral`, or any CSS colour, see
[ThemeColor.md](ThemeColor.md)). Text inside stays the page's text colour in every style. There
is no `Filled`: body text on a solid colour reads badly.

- `CardStyleType.Elevated` (default): a raised surface with a shadow, always neutral
- `CardStyleType.Outlined`: a border in the colour, no fill. In a colour it marks one card among its neighbours, such as the current plan. Say what the mark means in text as well (a subtitle or a Badge): the colour alone is not read out
- `CardStyleType.Light`: a flat tint of the colour, no border. In a colour it sets one card apart by a colour of its own, such as the recommended plan or a category

A message about something that happened (saved, failed, almost full) is not a coloured card but an
[Alert](Alert.md), which brings the icon, the screen-reader role and the dismiss button.

```tsx
import { Card, CardStyleType, CardHeader, CardMedia, CardContent, CardActions, ThemeColor } from '@ahrowe/ui';

// Elevated (default): fill and shadow
<Card>
  <CardHeader title="Elevated" subTitle="Fill and shadow" />
</Card>

// Outlined: transparent, defined by a border, no shadow
<Card styleType={CardStyleType.Outlined}>
  <CardHeader title="Outlined" />
</Card>

// Light: flat fill, no shadow
<Card styleType={CardStyleType.Light}>
  <CardHeader title="Light" />
</Card>

// The selected plan, bordered in the primary colour
<Card styleType={CardStyleType.Outlined} color={ThemeColor.Primary}>
  <CardHeader title="Pro" subTitle="Your current plan" />
</Card>

// The recommended plan, tinted
<Card styleType={CardStyleType.Light} color={ThemeColor.Primary}>
  <CardHeader title="Team" subTitle="Recommended for 5 people or more" />
</Card>

<Card hoverEffect onClick={handleClick}>
  <CardHeader
    title="Product name"
    subTitle="Category"
    thumbnail="https://example.com/image.jpg"
  />
  <CardMedia src="https://example.com/banner.jpg" alt="Banner" />
  <CardContent>
    <p>Description text goes here.</p>
  </CardContent>
  <CardActions
    actions={[
      { id: 'edit', label: 'Edit', onClick: handleEdit },
      { id: 'delete', label: 'Delete', color: ThemeColor.Error, onClick: handleDelete },
    ]}
  />
</Card>
```

All sub-components are optional — use only what you need. `CardActions` lays its buttons out in a row from the left, wrapping when they run out of room; its `actions` objects take no `disabled`, `icon` or `type`, so for a submit, loading or disabled button pass `Button`s as its children instead. Stacked sub-components are spaced apart automatically by a shared gap; pass `autoGap={false}` to remove it and control spacing yourself.

`CardContent` renders no element of its own, only its children, so it takes no `className` or
`style`, and `autoGap` spaces its children like the card's other children: one column,
`--spacing-s` apart. A field with a floating label (Input, Dropdown, TimeInput) draws it about 7px above its box, so a column of fields reads better at `--spacing-m`: put a column of your own inside it
(`display: flex; flex-direction: column; gap: var(--spacing-m)`). The card's own inner wrapper,
which holds every child including `CardContent`'s, is padded 1em; to change that, use its `content` slot
(`classNames.content` / `styles.content` on `Card`).

**Enums:**

- `CardStyleType`: `Elevated` (default) | `Outlined` | `Light`

**CardProps:**

| Prop | Type | Description |
|------|------|-------------|
| `styleType` | `CardStyleType` | `Elevated` (default, fill and shadow), `Outlined` (a border) or `Light` (a flat fill) |
| `color` | `ColorValue` | The border of `Outlined` and the tint of `Light`: a `ThemeColor` (default `Neutral`, the theme's card surfaces) or any CSS colour. `Elevated` ignores it |
| `onClick` | `() => void` | Makes the card clickable, by pointer and by Enter/Space. A click or key on a control inside the card (a field, a link, a button, anything with an interactive `role`) is that control's, and neither fires this nor ripples |
| `hoverEffect` | `boolean` | Lifts the card on hover and tints it in its text colour, over whatever its style and colour paint |
| `tabIndex` | `number \| null` | For keyboard navigation |
| `autoGap` | `boolean` | Space stacked sub-components apart with a shared gap (default `true`); set `false` to space them yourself |
| `clip` | `boolean` | Clip content to the rounded corners (`overflow: hidden`, default `false`). Set `true` for full-bleed `CardMedia` or a clipped hover/ripple effect. Safe to enable even when the card holds a `Dropdown`/`Popover`/`Tooltip` — those popovers are portaled and always escape the card regardless of this setting |
| `ref` | `Ref<HTMLDivElement>` | Ref to the root element |

**CardHeaderProps:**

| Prop | Type | Description |
|------|------|-------------|
| `title` | `ReactNode` | Main heading |
| `subTitle` | `ReactNode` | Secondary text |
| `thumbnail` | `string \| ReactNode` | URL → background image; ReactNode → custom element |

**CardActionsProps:**

| Prop | Type | Description |
|------|------|-------------|
| `actions` | `CardAction[]` | Array of `{ id, label, styleType, color, onClick }` renders Buttons (`Outlined` `Primary` unless set) |
| `children` | `ReactNode` | Custom content instead of `actions` |

## Sizing

Card is spaced by its font: the padding, the header's gap and thumbnail, the gap between title and subtitle, and the action row all scale with the theme's text size. The title is `1.125em` — 18px at a 16px base — rather than a fixed size, so it stays proportional to the body text beneath it.

`CardMedia` bleeds to the card's edges with a negative margin that has to stay equal to the padding, so change the two together.

`CardMedia` is an [`Image`](Image.md), so it takes every prop an Image does: `loading='lazy'`, `srcSet`, a `thumbnail`, a `fallback`. Give it the photo's `width` and `height` and it keeps their aspect ratio at the card's width, with a skeleton of that shape while it loads:

```tsx
<CardMedia src={product.photo} alt={product.name} width={1200} height={800} loading='lazy' />
```

**Slots:** `root` `content`

## Migration from `Filled`

Before 0.43 the flat style was called `Filled`. It is `Light` now, the name the library's other
components use for the same look. TypeScript points at each use:

| Before | After |
|--------|-------|
| `styleType={CardStyleType.Filled}` | `styleType={CardStyleType.Light}` |

`defaultTheme` and `lightTheme` still draw it in `Neutral` in `--background-accent`, through
`--background-card-filled`. A theme that does not set that variable now gets the see-through tint
of the text colour that every other `Light` uses, where it used to get `--background-accent`. On a
dark theme that tint is about as light as `Elevated`, so set the variable there to keep the two
apart.

A card with `hoverEffect` no longer turns `--background-accent` on hover: it keeps its own surface
and gets a light tint in its text colour over it, so a coloured card keeps its colour.
