# CTA Card

> An inset card with copy beside the actions and an optional square image.

- **Slug:** `cta-card`
- **Kind:** section · **Category:** Conversion
- **Source:** original to Viewership UI
- **Docs page:** https://viewership.ai/design/components/cta-card
- **All files in one bundle:** https://viewership.ai/design/components/cta-card.bundle.txt
- **Structured JSON:** https://viewership.ai/design/components/cta-card.json

## Install

1. Import the foundation tokens once (download: https://viewership.ai/design/raw/foundation.css.txt):

```astro
---
// src/layouts/Layout.astro
import "../styles/foundation.css";
---
```

2. Copy this file into `src/components/viewership/cta-card/`:

   - `CtaCard.astro` (raw: https://viewership.ai/design/raw/cta-card/CtaCard.astro.txt)

3. Copy its registry dependencies, each into its own sibling folder under `src/components/viewership/`:

   - `button`: `Button.astro` (docs: https://viewership.ai/design/components/button.md)

npm packages: none.

## Usage

```astro
---
import CtaCard from "../components/viewership/cta-card/CtaCard.astro";
---

<CtaCard />
```

## Example

The gallery demo, verbatim. Use the import path from Usage instead of `./`.

```astro
---
import CtaCard from "./CtaCard.astro";
---

<CtaCard
  title="Not sure what's wrong with it?"
  body="Send us a photo. A person on the bench will reply within a day."
  primary={{ label: "Send a photo", href: "#" }}
  secondary={{ label: "Read the care guides", href: "#" }}
>
  <img slot="media" src="/ui/demo/halftone-sun.webp" alt="" width="1200" height="1200" loading="lazy" />
</CtaCard>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` |  | Headline. |
| `accent` | `string` |  | Closing words of the headline. |
| `body` | `string` |  | Supporting line. |
| `primary` | `{ label: string; href: string }` |  | Main action. |
| `secondary` | `{ label: string; href: string }` |  | Text-style second action. |
| `as` | `"h2" \| "h3"` | `"h2"` | Heading element. |
| `slot: media` | `slot` |  | Optional square image at the start of the card. |

## Source

### `src/components/viewership/cta-card/CtaCard.astro`

```astro
---
/**
 * CtaCard: an inset card with copy on one side and the actions on the other,
 * plus an optional image strip. Sits on the page ground.
 *
 * @example
 * <CtaCard title="Not sure what's wrong with it?" primary={{ label: "Send a photo", href: "/help" }}>
 *   <img slot="media" src="/sun.webp" alt="" />
 * </CtaCard>
 */
import Button from "../button/Button.astro";

interface Cta {
  label: string;
  href: string;
}
interface Props {
  title: string;
  accent?: string;
  body?: string;
  primary: Cta;
  secondary?: Cta;
  as?: "h2" | "h3";
  class?: string;
}

const { title, accent, body, primary, secondary, as: Heading = "h2", class: className } = Astro.props;
const hasMedia = Astro.slots.has("media");
---

<section class:list={["vs-cta-card", { "has-media": hasMedia }, className]}>
  <div class="vs-cta-card__inner">
    <div class="vs-cta-card__panel">
      {
        hasMedia && (
          <div class="vs-cta-card__media">
            <slot name="media" />
          </div>
        )
      }
      <div class="vs-cta-card__copy">
        <Heading class="vs-display vs-cta-card__title">{title}{accent && <> <em>{accent}</em></>}</Heading>
        {body && <p class="vs-cta-card__body">{body}</p>}
      </div>
      <div class="vs-cta-card__actions">
        <Button href={primary.href} variant="primary">{primary.label}</Button>
        {secondary && <Button href={secondary.href} variant="quiet">{secondary.label}</Button>}
      </div>
    </div>
  </div>
</section>

<style>
  .vs-cta-card {
    container-type: inline-size;
    background: var(--vs-page);
    color: var(--vs-ink);
    font-family: var(--vs-font-sans);
    font-stretch: var(--vs-font-stretch);
  }
  .vs-cta-card__inner {
    max-width: var(--vs-container);
    margin-inline: auto;
    padding: var(--vs-section-y) var(--vs-gutter);
  }
  .vs-cta-card__panel {
    display: grid;
    gap: 24px;
    align-items: center;
    overflow: hidden;
    padding: clamp(24px, 5cqi, 56px);
    border: var(--vs-border-w) solid var(--vs-line);
    border-radius: var(--vs-radius-window);
    background: var(--vs-surface);
    box-shadow: var(--vs-pop-shadow, var(--vs-shadow-md));
  }
  .vs-cta-card__media {
    width: 96px;
    aspect-ratio: 1;
    overflow: hidden;
    border-radius: var(--vs-radius-card);
  }
  .vs-cta-card__media :global(img) {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
  .vs-cta-card__title {
    font-size: clamp(1.75rem, 4.5cqi, 3rem);
  }
  .vs-cta-card__body {
    max-width: 30rem;
    margin: 0.75rem 0 0;
    font-size: 1rem;
    line-height: 1.55;
    color: var(--vs-ink-2);
  }
  .vs-cta-card__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }
  @container (min-width: 760px) {
    .vs-cta-card__panel {
      grid-template-columns: minmax(0, 1fr) auto;
      gap: 40px;
    }
    .has-media .vs-cta-card__panel {
      grid-template-columns: auto minmax(0, 1fr) auto;
    }
    .vs-cta-card__media {
      width: 120px;
    }
  }
</style>
```
