# Testimonial Cards

> Quotes as an even row of cards with optional star ratings and initials avatars.

- **Slug:** `testimonial-cards`
- **Kind:** section · **Category:** Proof
- **Source:** original to Viewership UI
- **Docs page:** https://viewership.ai/design/components/testimonial-cards
- **All files in one bundle:** https://viewership.ai/design/components/testimonial-cards.bundle.txt
- **Structured JSON:** https://viewership.ai/design/components/testimonial-cards.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/testimonial-cards/`:

   - `TestimonialCards.astro` (raw: https://viewership.ai/design/raw/testimonial-cards/TestimonialCards.astro.txt)

npm packages: none.

## Usage

```astro
---
import TestimonialCards from "../components/viewership/testimonial-cards/TestimonialCards.astro";
---

<TestimonialCards />
```

## Example

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

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

<TestimonialCards
  title="Heard on"
  accent="the trail."
  items={[
    { quote: "We outfit twelve students a year. The fleet plan paid for itself in the first term.", name: "Tom Hendry", role: "Outdoor education lead", rating: 5 },
    { quote: "Boring answer: it works. Exciting answer: it works in sideways rain.", name: "Sam Whitlock", role: "Weekend camper", rating: 5 },
    { quote: "Heavier than the ultralight brands, and I have stopped caring.", name: "Ines Park", role: "Thru-hiker", rating: 4 },
  ]}
/>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` |  | Optional section headline. |
| `accent` | `string` |  | Closing words of the headline. |
| `items` | `{ quote: string; name: string; role?: string; rating?: number }[]` |  | Quotes. Three fill a row. |
| `as` | `"h2" \| "h3"` | `"h2"` | Heading element. |

## Source

### `src/components/viewership/testimonial-cards/TestimonialCards.astro`

```astro
---
/**
 * TestimonialCards: quotes as an even row of cards, each with a star rating
 * (optional) and an initials avatar.
 *
 * @example
 * <TestimonialCards title="Heard on" accent="the trail." items={[{ quote: "...", name: "Maya Ortiz", rating: 5 }]} />
 */
interface Item {
  quote: string;
  name: string;
  role?: string;
  /** 1 to 5. Omit to hide the stars. */
  rating?: number;
}
interface Props {
  title?: string;
  accent?: string;
  items: Item[];
  as?: "h2" | "h3";
  class?: string;
}

const { title, accent, items, as: Heading = "h2", class: className } = Astro.props;
const initials = (name: string) =>
  name
    .split(/\s+/)
    .map((p) => p[0])
    .slice(0, 2)
    .join("")
    .toUpperCase();
const star = "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z";
---

<section class:list={["vs-testimonial-cards", className]}>
  <div class="vs-testimonial-cards__inner">
    {title && <Heading class="vs-display vs-testimonial-cards__title">{title}{accent && <> <em>{accent}</em></>}</Heading>}
    <ul class="vs-testimonial-cards__grid">
      {
        items.map((q) => (
          <li>
            <figure class="vs-testimonial-cards__card">
              {q.rating && (
                <p class="vs-testimonial-cards__stars" role="img" aria-label={`${q.rating} out of 5 stars`}>
                  {Array.from({ length: 5 }, (_, i) => (
                    <svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" class:list={{ "is-off": i >= q.rating! }}>
                      <path d={star} />
                    </svg>
                  ))}
                </p>
              )}
              <blockquote>
                <p>{q.quote}</p>
              </blockquote>
              <figcaption class="vs-testimonial-cards__by">
                <span class="vs-testimonial-cards__avatar" aria-hidden="true">{initials(q.name)}</span>
                <span>
                  <span class="vs-testimonial-cards__name">{q.name}</span>
                  {q.role && <span class="vs-testimonial-cards__role">{q.role}</span>}
                </span>
              </figcaption>
            </figure>
          </li>
        ))
      }
    </ul>
  </div>
</section>

<style>
  .vs-testimonial-cards {
    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-testimonial-cards__inner {
    max-width: var(--vs-container);
    margin-inline: auto;
    padding: var(--vs-section-y) var(--vs-gutter);
  }
  .vs-testimonial-cards__title {
    margin-bottom: clamp(28px, 5cqi, 56px);
    font-size: clamp(2rem, 6.5cqi, 4.25rem);
  }
  .vs-testimonial-cards__grid {
    display: grid;
    gap: 16px;
    margin: 0;
    padding: 0;
    list-style: none;
  }
  @container (min-width: 640px) {
    .vs-testimonial-cards__grid {
      grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
    }
  }
  .vs-testimonial-cards figure,
  .vs-testimonial-cards blockquote,
  .vs-testimonial-cards p {
    margin: 0;
  }
  .vs-testimonial-cards__card {
    display: flex;
    height: 100%;
    flex-direction: column;
    gap: 18px;
    padding: 28px;
    border: var(--vs-border-w) solid var(--vs-line);
    border-radius: var(--vs-radius-card);
    background: var(--vs-surface);
    box-shadow: var(--vs-shadow-sm);
  }
  .vs-testimonial-cards__card blockquote {
    flex: 1;
    font-size: 1.0625rem;
    line-height: 1.5;
  }
  .vs-testimonial-cards__stars {
    display: flex;
    gap: 2px;
    color: var(--vs-accent);
  }
  .vs-testimonial-cards__stars svg {
    fill: currentColor;
  }
  .vs-testimonial-cards__stars .is-off {
    fill: var(--vs-line-strong);
  }
  .vs-testimonial-cards__by {
    display: flex;
    align-items: center;
    gap: 12px;
    padding-top: 18px;
    border-top: 1px solid var(--vs-line);
    font-size: 0.875rem;
  }
  .vs-testimonial-cards__by > span:last-child {
    display: flex;
    flex-direction: column;
  }
  .vs-testimonial-cards__avatar {
    display: inline-flex;
    width: 38px;
    height: 38px;
    flex: none;
    align-items: center;
    justify-content: center;
    border-radius: var(--vs-radius-pill);
    background: var(--vs-ink);
    color: var(--vs-ink-inverse);
    font-size: 0.75rem;
    font-weight: 600;
  }
  .vs-testimonial-cards__name {
    font-weight: 600;
  }
  .vs-testimonial-cards__role {
    color: var(--vs-ink-3);
  }
</style>
```
