# Feature Cards

> Features as a joined index grid: numbered cells sharing hairlines, display-face titles, optional tags and links.

- **Slug:** `feature-cards`
- **Kind:** section · **Category:** Features
- **Source:** original to Viewership UI
- **Docs page:** https://viewership.ai/ui/components/feature-cards
- **All files in one bundle:** https://viewership.ai/ui/components/feature-cards.bundle.txt
- **Structured JSON:** https://viewership.ai/ui/components/feature-cards.json

## Install

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

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

2. Copy this file into `src/components/viewership/feature-cards/`:

   - `FeatureCards.astro` (raw: https://viewership.ai/ui/raw/feature-cards/FeatureCards.astro.txt)

npm packages: none.

## Usage

```astro
---
import FeatureCards from "../components/viewership/feature-cards/FeatureCards.astro";
---

<FeatureCards />
```

## Example

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

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

<FeatureCards
  title="Six habits that"
  accent="make gear last."
  intro="No marketing gloss. This is what we do differently, and what it costs us to do it."
  items={[
    { title: "Repairs for life", body: "Send any pack back. We patch, re-stitch or replace the part and return it in two days.", tag: "Service", href: "#" },
    { title: "Small batches", body: "Forty runs a year, each sized to what we can inspect by hand.", tag: "Making" },
    { title: "Honest specs", body: "Weights, waterproof ratings and fabric sources on every product page.", tag: "Product" },
    { title: "Trail-tested", body: "A volunteer crew of guides signs off every design before it ships.", tag: "Making" },
    { title: "Ships from Duluth", body: "One warehouse, one team, orders out the same day before 2pm.", tag: "Service" },
    { title: "Resale built in", body: "Trade in a used piece and we refurbish and relist it.", tag: "Product", href: "#" },
  ]}
/>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` |  | Section headline. |
| `accent` | `string` |  | Closing words of the headline, drawn in the theme's accent style. |
| `intro` | `string` |  | Paragraph set beside the headline on wide screens. |
| `items` | `{ title: string; body: string; tag?: string; href?: string }[]` |  | The features. Three or six fill the grid evenly. |
| `linkLabel` | `string` | `"Learn more"` | Text of the per-cell link. |
| `as` | `"h2" \| "h3"` | `"h2"` | Heading element. |

## Source

### `src/components/viewership/feature-cards/FeatureCards.astro`

```astro
---
/**
 * FeatureCards: features as a joined index grid. Cells share hairlines like a
 * spec sheet, each with a number, an optional tag, a display-face title and
 * an optional link. Hover tints the cell.
 *
 * @example
 * <FeatureCards title="Six habits that" accent="make gear last." items={[
 *   { title: "Repairs for life", body: "Send any pack back.", tag: "Service", href: "/repairs" },
 * ]} />
 */
interface Item {
  title: string;
  body: string;
  /** Short label in the top corner, e.g. a category. */
  tag?: string;
  /** Adds a "Learn more" link; the whole cell becomes clickable. */
  href?: string;
}
interface Props {
  title: string;
  accent?: string;
  intro?: string;
  items: Item[];
  /** Label for the per-cell link. */
  linkLabel?: string;
  as?: "h2" | "h3";
  class?: string;
}

const { title, accent, intro, items, linkLabel = "Learn more", as: Heading = "h2", class: className } = Astro.props;
const num = (i: number) => String(i + 1).padStart(2, "0");
---

<section class:list={["vs-feature-cards", className]}>
  <div class="vs-feature-cards__inner">
    <header class="vs-feature-cards__head">
      <Heading class="vs-display vs-feature-cards__title">{title}{accent && <> <em>{accent}</em></>}</Heading>
      {intro && <p class="vs-feature-cards__intro">{intro}</p>}
    </header>
    <ul class="vs-feature-cards__grid">
      {
        items.map((item, i) => (
          <li class="vs-feature-cards__cell">
            <div class="vs-feature-cards__meta">
              <span class="vs-feature-cards__num">{num(i)}</span>
              {item.tag && <span class="vs-feature-cards__tag">{item.tag}</span>}
            </div>
            <h3 class="vs-display vs-feature-cards__name">{item.title}</h3>
            <p class="vs-feature-cards__body">{item.body}</p>
            {item.href && (
              <a class="vs-feature-cards__link" href={item.href}>
                {linkLabel}
                <span class="vs-sr-only">: {item.title}</span>
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                  <path d="M5 12h14" />
                  <path d="m12 5 7 7-7 7" />
                </svg>
              </a>
            )}
          </li>
        ))
      }
    </ul>
  </div>
</section>

<style>
  .vs-feature-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-feature-cards__inner {
    max-width: var(--vs-container);
    margin-inline: auto;
    padding: var(--vs-section-y) var(--vs-gutter);
  }
  .vs-feature-cards__head {
    display: grid;
    gap: 1.25rem;
    align-items: end;
  }
  .vs-feature-cards__title {
    font-size: clamp(2rem, 6.5cqi, 4.25rem);
  }
  .vs-feature-cards__intro {
    max-width: 26rem;
    margin: 0;
    font-size: 1rem;
    line-height: 1.55;
    color: var(--vs-ink-2);
  }
  @container (min-width: 820px) {
    .vs-feature-cards__head {
      grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
      gap: 48px;
    }
  }

  /* The grid draws its hairlines with a 1px gap over a line-coloured ground. */
  .vs-feature-cards__grid {
    display: grid;
    gap: var(--vs-border-w);
    margin: clamp(32px, 6cqi, 64px) 0 0;
    padding: 0;
    list-style: none;
    border: var(--vs-border-w) solid var(--vs-line-strong);
    border-radius: var(--vs-radius-card);
    background: var(--vs-line-strong);
    overflow: hidden;
    box-shadow: var(--vs-pop-shadow, none);
  }
  @container (min-width: 560px) {
    .vs-feature-cards__grid {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }
  @container (min-width: 900px) {
    .vs-feature-cards__grid {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
  }
  .vs-feature-cards__cell {
    position: relative;
    display: flex;
    min-height: 260px;
    flex-direction: column;
    padding: clamp(20px, 3cqi, 32px);
    background: var(--vs-surface);
    transition: background-color var(--vs-duration) var(--vs-ease-out);
  }
  .vs-feature-cards__cell:hover {
    background: var(--vs-inset);
  }
  .vs-feature-cards__cell:hover .vs-feature-cards__num {
    color: var(--vs-accent-ink);
  }
  .vs-feature-cards__meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding-bottom: 56px;
  }
  .vs-feature-cards__num {
    font-size: 0.8125rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--vs-ink-3);
    transition: color var(--vs-duration) var(--vs-ease-out);
  }
  .vs-feature-cards__tag {
    padding: 3px 10px;
    border: 1px solid var(--vs-line-strong);
    border-radius: var(--vs-radius-pill);
    font-size: 0.75rem;
    color: var(--vs-ink-2);
  }
  .vs-feature-cards__name {
    font-size: clamp(1.4rem, 2.6cqi, 1.75rem);
    line-height: 1.1;
  }
  .vs-feature-cards__body {
    margin: 10px 0 0;
    font-size: 0.9375rem;
    line-height: 1.55;
    color: var(--vs-ink-2);
  }
  .vs-feature-cards__link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    width: fit-content;
    margin-top: auto;
    padding-top: 20px;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--vs-ink);
    text-decoration: none;
  }
  .vs-feature-cards__link::after {
    content: "";
    position: absolute;
    inset: 0;
  }
  .vs-feature-cards__link svg {
    transition: transform var(--vs-duration) var(--vs-ease-out);
  }
  .vs-feature-cards__cell:hover .vs-feature-cards__link svg {
    transform: translateX(3px);
  }
  .vs-feature-cards__link:focus-visible {
    outline: none;
  }
  .vs-feature-cards__cell:has(.vs-feature-cards__link:focus-visible) {
    box-shadow: inset 0 0 0 2px var(--vs-accent);
  }
</style>
```
