Skip to content
Sign in
Generative UIProduct cardMenu

Product card

A shopping agent's result: photo, price and sale price in the right currency, a rating you read as a shape, color swatches and sizes that are real radio groups, stock, delivery, compare and an Add to cart that shows progress and confirms.

Three shoes with sale prices, color swatches and sizes, stock, delivery, compare and Add to cart.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/product-card.json

Usage

tsx
import { ProductCard } from "@/components/ui/chatcn/product-card"

<ProductCard
  title={product.title}
  merchant={product.store}
  image={product.image}
  price={product.price}
  salePrice={product.salePrice}
  currency="EUR"
  variants={[{ name: "Size", options: sizes.map((s) => ({ value: s, label: `EU ${s}` })) }]}
  stock="in-stock"
  onAddToCart={(selection) => cart.add(product.id, selection)}
/>

Every widget takes plain props from a tool result, so map the tool output straight onto it. While the call runs, pass loading for a skeleton that matches the final layout; when it fails, pass error (and onRetry). Install with product-card and nothing else from the kit is required at runtime.

Until every variant group has a choice, the button says what is missing (Select size) and stays disabled. Groups with one available option are chosen for you. When onAddToCart returns a promise, the label crossfades to Adding with a spinner, then to Added to cart; the button never changes width.

Examples

List rows

The same card as one row, for a ranked list next to the agent's answer.

States

Loading, adding, added, a store error, no match, and a missing image with long text.

Props

PropTypeNotes
title, merchant, hrefstringWithout a title the card shows the empty state
image, imageAltstringHolds a 4:3 frame; a broken image shows a quiet glyph
badgestringBest match, Lowest price
price, salePricenumberMajor units. A lower sale price strikes the regular one
currency, localestringIntl.NumberFormat currency formatting
rating, reviewCountnumberOut of 5. Large counts go compact: 12.9K
variants{ name, options: { value, label, swatch?, available? }[] }[]A swatch color makes a swatch picker
selection, defaultSelection, onSelectionChangeRecord<string, string>Keyed by group name
stock, stockCount"in-stock" | "low-stock" | "out-of-stock" | "preorder"Green, amber, gray or blue, each with an icon and words
shippingstringFree delivery Thu, Oct 29
onAddToCart, cartState"idle" | "adding" | "added"Uncontrolled from the promise, or controlled
comparable, compared, defaultCompared, onComparedChangebooleanCompare checkbox
layout"grid" | "list"Default grid
loading, error, onRetry, emptyStates
labelsPartial<ProductCardLabels>Every string the card shows or announces, for translation. Counts and names arrive as function arguments, so the word order is yours

Keyboard

KeyAction
TabTitle link, each variant group, compare, Add to cart
ArrowLeft, ArrowRightMove through the options in a variant group
SpaceToggle compare
EnterAdd to cart

Accessibility

  • Variant pickers are native radio groups inside a fieldset, so the group name and each option's name (Ember red, EU 42, sold out) are read without extra work. Swatches never rely on color alone.
  • Prices read as Sale price and Regular price. The stars are hidden; the rating and review count are read as text.
  • Adding and Added to cart are announced through a polite status region.

Motion

  • The cart label crossfades with a 2 px blur in 150 ms. Under reduced motion it swaps instantly.
  • Photos fade in over 200 ms once loaded. Cards never lift or scale on hover.