# Card (/ui/components/react/card)




## Usage [#usage]

```tsx
import { Card, CardMedia, CardHeader, CardTitle, CardDescription, CardFooter } from '@appica/ui-react/card'
```

```tsx
<Card>
  <CardHeader>
    <CardTitle>Weekly digest</CardTitle>
    <CardDescription>A short round-up of what changed and who shipped it.</CardDescription>
  </CardHeader>
  <CardFooter>
    <Button>Read digest</Button>
  </CardFooter>
</Card>
```

`Card` is a **layout container**, not a widget - it has no state, no interaction, and stays renderable from a Server Component. Compose it from its parts, all under `@appica/ui-react/card`:

* **`Card`** - the root. Owns the `frame`, the `inset`, and the radius every other corner scales from. It renders two elements: the outer frame and the inner **content** wrapper, a flex column that holds the slots.
* **`CardMedia`** - a clipped box for an image or video. Place it above the header or below the footer.
* **`CardHeader`** - the text column. It stretches, which is what pins `CardFooter` to the bottom of a fixed-height card.
* **`CardTitle`** - the headline (`<h3>`).
* **`CardDescription`** - the supporting text (`<p>`).
* **`CardFooter`** - a row for actions that stacks on narrow viewports.

Every part is optional. A card can be nothing but a `CardMedia`, or nothing but a `CardHeader`.

### One radius, everywhere [#one-radius-everywhere]

`--card-radius` is the card's own corner (the one on the content wrapper) and the only number you ever set:

```tsx
<Card className="[--card-radius:var(--radius-3xl)]">
```

The frame and the inset media scale off it **proportionally**: the frame rounds at `×4/3` and inset media at `×3/4`. That's deliberate - subtracting the padding instead would leave inset media on a hairline corner at the default radius, and collapse it entirely below that. Retune one value and the whole nest stays in proportion.

> **Tip:**
>
> Set the radius through `--card-radius` rather than a `rounded-*` utility. A `rounded-*` class only paints the element
> you put it on; the variable is what the frame and media scale from.

## Examples [#examples]

### Default [#default]

The most basic setup - no props, just a header and a footer.

```tsx
import { Card, CardHeader, CardTitle, CardDescription, CardFooter } from '@appica/ui-react/card'
import { Button } from '@appica/ui-react/button'

export default function CardDefault() {
  return (
    <Card className="w-full max-w-90">
      <CardHeader>
        <CardTitle>Weekly digest</CardTitle>
        <CardDescription>
          A short round-up of what changed, who shipped it, and what still needs a second pair of eyes.
        </CardDescription>
      </CardHeader>
      <CardFooter>
        <Button>Read digest</Button>
      </CardFooter>
    </Card>
  )
}
```

### Frame [#frame]

`frame="solid"` wraps the content in a band of `background-subtle` and softens the inner hairline to `border-muted`. Leave it off (the default) unless the card needs that extra separation from the page.

```tsx
import { Card, CardHeader, CardTitle, CardDescription, CardFooter } from '@appica/ui-react/card'
import { Button } from '@appica/ui-react/button'

export default function CardFrame() {
  return (
    <div className="grid w-full gap-4 sm:grid-cols-2">
      <Card>
        <CardHeader>
          <CardTitle>No frame</CardTitle>
          <CardDescription>The card sits directly on the page, outlined by a single hairline border.</CardDescription>
        </CardHeader>
        <CardFooter>
          <Button variant="soft">Learn more</Button>
        </CardFooter>
      </Card>

      <Card frame="solid">
        <CardHeader>
          <CardTitle>Solid frame</CardTitle>
          <CardDescription>
            An 8px band of <code>background-subtle</code> wraps the content and lifts the card off the page.
          </CardDescription>
        </CardHeader>
        <CardFooter>
          <Button variant="outline">Learn more</Button>
        </CardFooter>
      </Card>
    </div>
  )
}
```

### Glass frame [#glass-frame]

`frame="glass"` swaps the band for a translucent, blurred one and drops the inner border. Glass only exists relative to what's behind it - put the card over a photo, a gradient, or a colored section, never on flat page background.

```tsx
import { Card, CardHeader, CardTitle, CardDescription, CardFooter } from '@appica/ui-react/card'
import { Button } from '@appica/ui-react/button'

export default function CardGlass() {
  return (
    <div className="w-full rounded-2xl bg-[image:linear-gradient(rgb(0_0_0/0.2),rgb(0_0_0/0.2)),url('/carousel/slide-2.jpg')] bg-cover bg-center p-8">
      <Card frame="glass" className="mx-auto w-full max-w-90">
        <CardHeader>
          <CardTitle>A room above the clouds</CardTitle>
          <CardDescription>
            The glass frame borrows the pixels behind it, so it only reads as glass over imagery or a colored surface.
          </CardDescription>
        </CardHeader>
        <CardFooter>
          <Button>Check availability</Button>
        </CardFooter>
      </Card>
    </div>
  )
}
```

### Media [#media]

`CardMedia` clips whatever you put in it, above the header or below the footer. With the default `inset`, media floats inside the content wrapper with all four corners rounded. With `inset={false}`, the header and footer take the full gutter themselves and media runs edge to edge - the content wrapper's own `overflow-hidden` clips it, so a top image rounds only its top corners and a bottom image only its bottom ones, in either writing direction.

```tsx
import { Card, CardMedia, CardHeader, CardTitle, CardDescription } from '@appica/ui-react/card'

export default function CardMediaExample() {
  return (
    <div className="grid w-full gap-4 sm:grid-cols-2">
      <Card>
        <CardMedia className="h-44">
          <img src="/carousel/slide-1.jpg" alt="Snow-capped peaks at sunrise" />
        </CardMedia>
        <CardHeader>
          <CardTitle>Inset</CardTitle>
          <CardDescription>
            The image floats 8px inside the content wrapper and keeps all four corners rounded.
          </CardDescription>
        </CardHeader>
      </Card>

      <Card inset={false}>
        <CardMedia className="h-44">
          <img src="/carousel/slide-1.jpg" alt="Snow-capped peaks at sunrise" />
        </CardMedia>
        <CardHeader>
          <CardTitle>Flush</CardTitle>
          <CardDescription>The image runs edge to edge and the card clips it to its own top corners.</CardDescription>
        </CardHeader>
      </Card>
    </div>
  )
}
```

### Video [#video]

Anything inside `CardMedia` is clipped the same way. Direct `<img>` and `<video>` children are stretched and `object-cover`'d for you, so a fixed height on `CardMedia` is all the sizing you need.

```tsx
import { Card, CardMedia, CardHeader, CardTitle, CardDescription } from '@appica/ui-react/card'

export default function CardVideo() {
  return (
    <Card className="w-full max-w-90">
      <CardMedia className="h-48">
        <video src="/card/video.mp4" autoPlay loop muted playsInline aria-label="A jellyfish drifting in dark water" />
      </CardMedia>
      <CardHeader>
        <CardTitle>Ambient loop</CardTitle>
        <CardDescription>
          A video element is clipped by exactly the same rounded box as an image - no extra classes needed.
        </CardDescription>
      </CardHeader>
    </Card>
  )
}
```

### Horizontal layout (Next.js Image / Link) [#horizontal-layout-nextjs-image--link]

There's no `orientation` prop - the content wrapper is a flex column, so turning a card sideways is one Tailwind class on it via `contentProps`. Wrap the header and footer in a plain `div` so they keep stacking while the media sits alongside.

`CardMedia` is already `position: relative`, so a `next/image` with `fill` drops straight in. The whole card renders as an `<article>` via `render`, and the title wraps a `next/link` so only the headline is clickable - see [Accessibility](#accessibility) for why that beats an `onClick` on the root.

```tsx
import Image from 'next/image'
import Link from 'next/link'
import { Card, CardMedia, CardHeader, CardTitle, CardDescription, CardFooter } from '@appica/ui-react/card'
import { Avatar, AvatarImage, AvatarFallback } from '@appica/ui-react/avatar'
import { Badge } from '@appica/ui-react/badge'
import { Button } from '@appica/ui-react/button'
import { Toggle } from '@appica/ui-react/toggle'
import { Bookmark, BookmarkFilled } from '@appica/icons-react'

export default function CardHorizontal() {
  return (
    <Card render={<article />} contentProps={{ className: 'md:flex-row md:gap-2' }} className="w-full max-w-160">
      <CardMedia className="h-48 shrink-0 md:h-auto md:w-56">
        <Image
          src="/carousel/slide-5.jpg"
          alt="A lone peak mirrored in a still lake"
          fill
          sizes="(min-width: 640px) 224px, 100vw"
          className="object-cover"
        />
      </CardMedia>
      <div className="flex min-w-0 flex-1 flex-col">
        <CardHeader>
          <Badge variant="secondary" size="sm" className="mb-1 self-start">
            Field notes
          </Badge>
          <CardTitle>
            <Link href="#" className="outline-ring rounded-xs hover:underline">
              What eight seasons above the treeline taught us about packing light
            </Link>
          </CardTitle>
          <CardDescription>
            Every gram you carry is a decision you already made. Here is the kit that survived - and the gear that never
            left the tent twice.
          </CardDescription>
        </CardHeader>
        <CardFooter className="flex-row items-center justify-between">
          <div className="flex min-w-0 items-center gap-2">
            <Avatar size="xs">
              <AvatarImage src="/avatars/01.jpg" alt="Sarah Jenkins" />
              <AvatarFallback>EV</AvatarFallback>
            </Avatar>
            <span className="text-foreground-muted truncate text-xs">Sarah Jenkins · 12 March 2026</span>
          </div>
          <Toggle
            aria-label="Save for later"
            className="data-pressed:text-foreground-intense"
            render={
              <Button variant="ghost" size="icon-sm">
                <Bookmark className="in-data-pressed:hidden" />
                <BookmarkFilled className="hidden in-data-pressed:block" />
              </Button>
            }
          />
        </CardFooter>
      </div>
    </Card>
  )
}
```

### Overlay [#overlay]

For a card that *is* an image, position `CardMedia` absolutely - `inset-2` when the card is inset, `inset-0` when it isn't - and let `CardHeader` stack above it. Add a scrim inside the media for legible text.

```tsx
import { Card, CardMedia, CardHeader, CardTitle, CardDescription } from '@appica/ui-react/card'
import { Badge } from '@appica/ui-react/badge'

export default function CardOverlay() {
  return (
    <div className="grid w-full gap-4 sm:grid-cols-2">
      {[true, false].map((inset) => (
        <Card key={String(inset)} inset={inset} className="h-72" contentProps={{ className: 'relative' }}>
          <CardMedia className={inset ? 'absolute inset-2' : 'absolute inset-0'}>
            <img src="/carousel/slide-7.jpg" alt="A lone mountain under towering clouds" />
            <div className="absolute inset-0 bg-linear-to-t from-black/80 via-black/25 to-transparent" />
          </CardMedia>
          <CardHeader className="relative justify-between">
            <Badge variant="light" className="self-start">
              {inset ? 'inset' : 'inset={false}'}
            </Badge>
            <div className="flex flex-col gap-1.5">
              <CardTitle className="text-white">The Lone Peak</CardTitle>
              <CardDescription className="text-white/75">
                The header stacks above an absolutely-positioned media layer, so the whole card becomes the image.
              </CardDescription>
            </div>
          </CardHeader>
        </Card>
      ))}
    </div>
  )
}
```

### Profile [#profile]

Any content is welcome inside `CardHeader`; it's a plain flex column. Here an [`Avatar`](/ui/components/react/avatar) leads a centered profile card.

```tsx
import { Card, CardHeader, CardTitle, CardDescription, CardFooter } from '@appica/ui-react/card'
import { Avatar, AvatarImage, AvatarFallback, AvatarBadge } from '@appica/ui-react/avatar'
import { Button } from '@appica/ui-react/button'

export default function CardProfile() {
  return (
    <Card frame="solid" className="w-full max-w-80">
      <CardHeader className="items-center text-center">
        <Avatar size="xl" className="mb-2">
          <AvatarImage src="/avatars/03.jpg" alt="" />
          <AvatarFallback>MR</AvatarFallback>
          <AvatarBadge />
        </Avatar>
        <CardTitle>Mateo Rossi</CardTitle>
        <CardDescription>Principal designer · Bologna</CardDescription>
      </CardHeader>
      <CardFooter className="sm:justify-center">
        <Button size="sm">Follow</Button>
        <Button variant="outline" size="sm">
          Message
        </Button>
      </CardFooter>
    </Card>
  )
}
```

### Empty state [#empty-state]

Reach `contentProps` to restyle the surface itself - a dashed border and a subtle fill turn a card into a placeholder. A [`Thumbnail`](/ui/components/react/thumbnail) in an icon variant carries the visual weight.

```tsx
import { Card, CardHeader, CardTitle, CardDescription, CardFooter } from '@appica/ui-react/card'
import { Thumbnail } from '@appica/ui-react/thumbnail'
import { Button } from '@appica/ui-react/button'
import { FolderPlus, Plus } from '@appica/icons-react'

export default function CardEmptyState() {
  return (
    <Card
      className="w-full max-w-110"
      contentProps={{ className: 'bg-background-subtle items-center border-dashed border-strong text-center' }}
    >
      <CardHeader className="items-center">
        <Thumbnail variant="icon-outline" className="mb-2">
          <FolderPlus />
        </Thumbnail>
        <CardTitle>No collections yet</CardTitle>
        <CardDescription className="max-w-70">
          Collections group related boards so your team can find them without digging through search.
        </CardDescription>
      </CardHeader>
      <CardFooter>
        <Button size="sm">
          <Plus data-icon="start" />
          New collection
        </Button>
      </CardFooter>
    </Card>
  )
}
```

### Product card [#product-card]

`CardHeader` is a plain flex column, so anything can live in it - here a price row, a [`ToggleGroup`](/ui/components/react/toggle-group) of sizes, and a [`NumberField`](/ui/components/react/number-field). The badge and wishlist toggle are positioned inside `CardMedia`, which is already `relative`.

```tsx
import Image from 'next/image'
import { Card, CardMedia, CardHeader, CardTitle, CardDescription, CardFooter } from '@appica/ui-react/card'
import { Badge } from '@appica/ui-react/badge'
import { Button } from '@appica/ui-react/button'
import { NumberField } from '@appica/ui-react/number-field'
import { Toggle } from '@appica/ui-react/toggle'
import { ToggleGroup } from '@appica/ui-react/toggle-group'
import { Heart, HeartFilled, ShoppingCartPlus } from '@appica/icons-react'

const SIZES = ['40', '41', '42', '43']

export default function CardProduct() {
  return (
    <Card className="w-full max-w-85">
      <CardMedia className="h-44">
        <Image
          src="/landing/nimbus-runner.jpg"
          alt="Nimbus Runner sneaker on a pastel background"
          fill
          sizes="340px"
          className="object-cover"
        />
        <Badge variant="light" size="sm" className="absolute inset-s-2 top-2">
          -20%
        </Badge>
        <Toggle
          aria-label="Add to wishlist"
          className="absolute inset-e-2 top-2"
          render={
            <Button variant="light" size="icon-sm">
              <Heart className="in-data-pressed:hidden" />
              <HeartFilled className="hidden in-data-pressed:block" />
            </Button>
          }
        />
      </CardMedia>

      <CardHeader>
        <div className="flex items-start justify-between gap-3">
          <div>
            <CardTitle className="text-base">Nimbus Runner</CardTitle>
            <CardDescription className="text-xs">Men&apos;s running shoes</CardDescription>
          </div>
          <div className="text-end">
            <div className="text-foreground-intense font-semibold tabular-nums">$96</div>
            <s className="text-foreground-muted text-xs tabular-nums">$120</s>
          </div>
        </div>

        <div className="mt-3 flex items-center justify-between gap-3">
          <ToggleGroup aria-label="Shoe size" defaultValue={['42']}>
            {SIZES.map((size) => (
              <Toggle
                key={size}
                value={size}
                render={
                  <Button variant="outline" size="icon-sm" className="text-xs">
                    {size}
                  </Button>
                }
              />
            ))}
          </ToggleGroup>
          <NumberField variant="soft" size="sm" defaultValue={1} min={1} max={9} aria-label="Quantity" />
        </div>
      </CardHeader>

      <CardFooter>
        <Button size="sm" className="w-full">
          <ShoppingCartPlus data-icon="start" />
          Add to cart
        </Button>
      </CardFooter>
    </Card>
  )
}
```

### Form in a card [#form-in-a-card]

`render={<form />}` makes the card itself the form element, so [`Field`](/ui/components/react/field)s and the submit button need no extra wrapper.

```tsx
import { Card, CardHeader, CardTitle, CardDescription, CardFooter } from '@appica/ui-react/card'
import { Field, FieldLabel } from '@appica/ui-react/field'
import { Input } from '@appica/ui-react/input'
import { Checkbox } from '@appica/ui-react/checkbox'
import { Button } from '@appica/ui-react/button'

function AppicaMark() {
  return (
    <svg className="size-9" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" fill="none" aria-hidden="true">
      <path
        className="text-primary"
        d="M16 0c11.671 0 16 4.329 16 16s-4.329 16-16 16S0 27.671 0 16 4.329 0 16 0z"
        fill="currentColor"
      />
      <path
        className="text-primary-foreground"
        d="M20.27 8.945c-1.793-2.54-5.534-2.54-7.326 0l-6.023 8.534c-2.118 3.001.01 7.162 3.663 7.162h4.476c-.849-1.301-1.344-2.859-1.344-4.533 0-4.56 3.666-8.257 8.189-8.257.142 0 .282.004.422.011L20.27 8.945zm2.117 14.886c1.951 0 3.533-1.595 3.533-3.562s-1.582-3.562-3.533-3.562-3.533 1.595-3.533 3.562 1.582 3.562 3.533 3.562z"
        fill="currentColor"
      />
    </svg>
  )
}

export default function CardSignIn() {
  return (
    <Card frame="solid" render={<form />} className="w-full max-w-90">
      <CardHeader>
        <AppicaMark />
        <CardTitle className="mt-1 text-xl">Sign in to Appica</CardTitle>
        <CardDescription>Sign in to view order status, update billing, check past orders.</CardDescription>

        <div className="mt-4 flex flex-col gap-4">
          <Field name="email">
            <FieldLabel>Email</FieldLabel>
            <Input type="email" variant="soft" autoComplete="email" />
          </Field>
          <Field name="password">
            <div className="flex items-center justify-between gap-3">
              <FieldLabel>Password</FieldLabel>
              <a href="#!" className="outline-ring text-foreground-intense rounded-xs text-sm underline">
                Forgot password?
              </a>
            </div>
            <Input type="password" variant="soft" autoComplete="current-password" />
          </Field>
          <label className="flex items-center gap-2 text-sm select-none">
            <Checkbox name="persist" defaultChecked />
            Stay signed in
          </label>
        </div>
      </CardHeader>
      <CardFooter>
        <Button type="submit" className="w-full">
          Sign in
        </Button>
      </CardFooter>
    </Card>
  )
}
```

### Radius [#radius]

Changing `--card-radius` retunes the card and its inset media together, in one place.

```tsx
import { Card, CardMedia, CardHeader, CardTitle } from '@appica/ui-react/card'

const RADII = [
  { label: 'radius-sm', className: '[--card-radius:var(--radius-sm)]' },
  { label: 'radius-xl (default)', className: '' },
  { label: 'radius-3xl', className: '[--card-radius:var(--radius-3xl)]' },
]

export default function CardRadius() {
  return (
    <div className="grid w-full gap-4 sm:grid-cols-2 md:grid-cols-3">
      {RADII.map((radius) => (
        <Card key={radius.label} className={radius.className}>
          <CardMedia className="h-24">
            <img src="/carousel/slide-4.jpg" alt="" />
          </CardMedia>
          <CardHeader>
            <CardTitle className="text-base">{radius.label}</CardTitle>
          </CardHeader>
        </Card>
      ))}
    </div>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

`Card` is built entirely from logical padding and flex ordering, so the footer's actions flow from the right and text aligns to the start edge with no direction-specific classes. Flush media is clipped by the content wrapper rather than by fixed top/bottom radii, so it follows the writing direction too. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).


## API reference [#api-reference]

### Card [#card]

The root. Renders the outer frame plus an inner content wrapper, and publishes the one custom property the other parts scale from.

| Prop           | Type   | Default | Description                                                          |
| -------------- | --------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
| `frame`        | `boolean \| 'solid' \| 'glass'`    | `false` | Wrap the content in a padded frame. `true` is an alias for `'solid'`; `'glass'` is translucent and blurred. |
| `inset`        | `boolean`                                     | `true`  | Float the slots inside the content, so media rounds all four corners. `false` gives edge-to-edge media.     |
| `contentProps` | `ComponentPropsWithoutRef\<'div'>` | -       | Escape hatch for the inner content wrapper - e.g. `{'{ className: "sm:flex-row" }'}` for a horizontal card. |
| `render`       | `ReactElement`                                | -       | Render as a different element, e.g. `render={<article />}` or `render={<li />}`.                            |
| `className`    | `string`                                      | -       | Extra classes on the root, merged via `tailwind-merge`.                                                     |

Any other `<div>` attribute is forwarded to the root.

#### `--card-radius` [#--card-radius]

The card's corner radius, defaulting to `var(--radius-xl)`. Set it with an arbitrary-property class on `Card` (or anywhere above it) and the frame (`×4/3`) and inset media (`×3/4`) scale with it.

```tsx
<Card className="[--card-radius:var(--radius-2xl)]">
```

### CardMedia [#cardmedia]

A clipped box for an image or video. Renders a `<div>`; direct `<img>` and `<video>` children get `size-full object-cover`.

| Prop        | Type        | Default | Description                                                     |
| ----------- | ----------- | ------- | --------------------------------------------------------------- |
| `children`  | `ReactNode` | -       | The image, video, and any overlay layers.                       |
| `className` | `string`    | -       | Extra classes - commonly a height. Merged via `tailwind-merge`. |

### CardHeader [#cardheader]

The text column. Renders a `<div>` that stretches to fill the content wrapper, which is what pins the footer down.

| Prop        | Type        | Default | Description                                 |
| ----------- | ----------- | ------- | ------------------------------------------- |
| `children`  | `ReactNode` | -       | Title, description, and any custom content. |
| `className` | `string`    | -       | Extra classes, merged via `tailwind-merge`. |

### CardTitle [#cardtitle]

The headline. Renders an `<h3>`.

| Prop        | Type           | Default | Description                                                                 |
| ----------- | -------------- | ------- | --------------------------------------------------------------------------- |
| `children`  | `ReactNode`    | -       | The title text.                                                             |
| `render`    | `ReactElement` | -       | Render as another element, e.g. `render={<h2 />}`, to fit the page outline. |
| `className` | `string`       | -       | Extra classes, merged via `tailwind-merge`.                                 |

### CardDescription [#carddescription]

The supporting text. Renders a `<p>` styled `text-foreground-muted text-sm` to pair with `CardTitle`; override with `className`.

| Prop        | Type           | Default | Description                                 |
| ----------- | -------------- | ------- | ------------------------------------------- |
| `children`  | `ReactNode`    | -       | The body text.                              |
| `render`    | `ReactElement` | -       | Render as another element.                  |
| `className` | `string`       | -       | Extra classes, merged via `tailwind-merge`. |

### CardFooter [#cardfooter]

A row for actions. Renders a `<div>` that stacks in reverse order below the `sm` breakpoint, so the primary action sits on top on narrow screens.

| Prop        | Type        | Default | Description                                 |
| ----------- | ----------- | ------- | ------------------------------------------- |
| `children`  | `ReactNode` | -       | Buttons or any trailing content.            |
| `className` | `string`    | -       | Extra classes, merged via `tailwind-merge`. |

## Accessibility [#accessibility]

* `Card` renders a plain `<div>` with no implicit role - it's a visual grouping, not a landmark. Use `render={<article />}` when the card is self-contained content, or `render={<li />}` inside a list, so the structure is exposed to assistive tech.
* `CardTitle` is an `<h3>` so a page of cards reads as a coherent outline. If `<h3>` is the wrong level for your page, change it with `render={<h2 />}` rather than restyling a `<div>`.
* Make the **whole card** clickable only if you also give it a real control: put an `<a>` or `<button>` on the title and stretch its hit area (`after:absolute after:inset-0`) instead of adding `onClick` to the root - that keeps the link keyboard-reachable and announced.
* Decorative media should carry `alt=""` so screen readers skip it; describe meaningful imagery in the `alt` text, not in the card copy alone.
* A video that autoplays in `CardMedia` must be `muted`, `playsInline`, short, and purely decorative - Safari won't even paint a frame otherwise. Anything the user is meant to actually watch needs `controls` and no `autoPlay`.
* The card adds no animation, so there is nothing to suppress for users who prefer reduced motion.
