# Product Card

> Product tile for catalogs and grids, plus the pieces it is made of: Price, Rating, StockBadge and ProductArt.

Component · Commerce · Pro kit · [Live page](https://ui.twentyfirst.media/pro/components/ProductCard/)

Product tile for catalogs and grids, plus the pieces it is made of: `Price`, `Rating`, `StockBadge` and `ProductArt`.

ProductCard props: `name`, `meta`, `art` + `tint` (or swap in your own image inside `.tf-product-media`), `price`, `compareAt`, `rating`, `reviews`, `badge` + `badgeTone`, `stock`, `wished` + `onWish`, `added` + `onAdd`, `onClick`.
Price: `value`, `compareAt` (shows the struck price and −%), `size` ("lg"), `currency`, `showSave`. Rating: `value`, `count`, `size`. StockBadge: `stock`, `low` (10), `compact`. ProductArt: `kind` (tee, mug, bottle, cap, tote, sneaker, watch, lamp), `tint`, `size`.

- One badge per card at most (Bestseller, New, Sold out). Sale is shown by the price, not a badge.
- Quick-add only for products without variants; otherwise open the product page.
- Low stock is a warning badge with the number left; out of stock disables add.

## Example

```tsx
import * as React from "react";
import { Price, ProductArt, ProductCard, Rating, StockBadge } from "@twentyfirstmedia/pro";
import type { ProductCardProps } from "@twentyfirstmedia/pro";

function App() {
  const w = React.useState<string[]>(["p2"]),
    a = React.useState<string[]>([]);
  const P: (ProductCardProps & { id: string })[] = [
    {
      id: "p1",
      name: "Halo Cotton Tee",
      meta: "Apparel",
      art: "tee",
      tint: "sky",
      price: 32,
      compareAt: 39,
      rating: 4.7,
      reviews: 128,
      badge: "Bestseller",
    },
    {
      id: "p2",
      name: "Studio Mug",
      meta: "Home",
      art: "mug",
      tint: "butter",
      price: 18,
      rating: 4.9,
      reviews: 96,
    },
    {
      id: "p3",
      name: "Field Bottle",
      meta: "Outdoor",
      art: "bottle",
      tint: "mint",
      price: 28,
      rating: 4.6,
      reviews: 61,
      badge: "New",
    },
    {
      id: "p4",
      name: "Canvas Tote",
      meta: "Bags",
      art: "tote",
      tint: "lilac",
      price: 24,
      rating: 4.8,
      reviews: 143,
      stock: 0,
      badge: "Sold out",
      badgeTone: "neutral",
    },
  ];
  return (
    <div
      className="tf-grid"
      style={
        {
          "--cols": "repeat(4, minmax(0, 1fr))",
          "--cols-sm": "repeat(2, minmax(0, 1fr))",
          "--cols-md": "repeat(2, minmax(0, 1fr))",
          gap: 16,
        } as React.CSSProperties
      }
    >
      {P.map((x) => (
        <ProductCard
          key={x.id}
          {...x}
          wished={w[0].indexOf(x.id) > -1}
          added={a[0].indexOf(x.id) > -1}
          onWish={() => {
            w[1](w[0].indexOf(x.id) > -1 ? w[0].filter((i) => i !== x.id) : w[0].concat([x.id]));
          }}
          onAdd={() => {
            a[1](a[0].concat([x.id]));
          }}
        />
      ))}
      <div
        className="tf-panel"
        style={{
          gridColumn: "1 / -1",
          flexDirection: "row",
          flexWrap: "wrap",
          gap: 24,
          alignItems: "center",
        }}
      >
        <Price value={32} compareAt={39} size="lg" />
        <Rating value={4.6} count={61} />
        <StockBadge stock={84} />
        <StockBadge stock={6} />
        <StockBadge stock={0} />
        <div
          style={{
            display: "flex",
            flexWrap: "wrap",
            gap: 8,
          }}
        >
          {ProductArt.kinds.map((k, i) => (
            <ProductArt
              key={k}
              kind={k}
              tint={(["sky", "mint", "butter", "lilac", "blush"] as const)[i % 5]}
              size={40}
            />
          ))}
        </div>
      </div>
    </div>
  );
}
export default App;
```

## Props

### Price

| Prop | Type | Notes |
| --- | --- | --- |
| `value` | `number` | Required |
| `compareAt?` | `number` |  |
| `currency?` | `string` |  |
| `size?` | `"lg"` |  |
| `showSave?` | `boolean` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### ProductArt

| Prop | Type | Notes |
| --- | --- | --- |
| `kind` | `ArtKind` | Required |
| `tint?` | `Tint \| "plain"` |  |
| `size?` | `number` |  |
| `label?` | `string` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### ProductCard

| Prop | Type | Notes |
| --- | --- | --- |
| `name` | `string` | Required |
| `meta?` | `string` |  |
| `art` | `ArtKind` | Required |
| `tint?` | `Tint` |  |
| `price` | `number` | Required |
| `compareAt?` | `number` |  |
| `rating?` | `number` |  |
| `reviews?` | `number` |  |
| `badge?` | `string` |  |
| `badgeTone?` | `Tone` |  |
| `stock?` | `number` |  |
| `wished?` | `boolean` |  |
| `onWish?` | `() => void` |  |
| `added?` | `boolean` |  |
| `onAdd?` | `() => void` |  |
| `onClick?` | `() => void` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Rating

| Prop | Type | Notes |
| --- | --- | --- |
| `value` | `number` | Required |
| `count?` | `number` |  |
| `size?` | `number` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### StockBadge

| Prop | Type | Notes |
| --- | --- | --- |
| `stock` | `number` | Required |
| `low?` | `number` |  |
| `compact?` | `boolean` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
