Skip to content

ProductCard

A product in a grid or a carousel, linked to href: image, ribbon, name, price and availability, with what layout adds for a consumer or a business buyer, and slots for what a site adds — ribbon, compare, swatches, packSize, actions, availability, hoverImage. The price row sits at the foot, so prices line up across a row of cards. Server-renderable; routing and the image component are the app's. Inside a ProductListingLayout set to list it lays out as a row.

import { ProductCard } from "@evolve-storefront/feature-product/product-card";

Renders inside ProductListingProvider.

Preview

DefaultOpen in Storybook ↗

Usage

Image, ribbon, name, price and availability; both links lead to the product.

<ProductCard
product={variant}
href={productHref(variant.product.slug)}
onLinkClick={() => emit("productClicked", { product, list, position })}
renderLink={({ href, children, ...props }) => <Link href={href} {...props}>{children}</Link>}
renderImage={(props) => <Image {...props} width={352} height={235} />}
actions={<ShoppingListButton sku={variant.sku} />}
/>;

Props

PropTypeDescription
productProductCardData
hrefstring

The product page — the app's link builder.

onLinkClick?MouseEventHandler<HTMLAnchorElement>

Both links report a click through this — productClicked, with the list and position.

renderLink?((props: ProductCardLinkProps) => ReactNode)
renderImage?((props: ProductCardImageRenderProps) => ReactNode)
hoverImage?{ url: string; } | null

Fades in over the image on hover: productCardHoverImage(product).

loading?"lazy" | "eager"
headingLevel?HeadingLevel
ribbon?ReactNode

Replaces the product's own ribbon.

compare?ReactNode

A compare toggle, above the name — not over the image, whose link would crowd a control too small to tap apart from it.

swatches?ReactNode

Above the brand: the colors it comes in, <ProductCardColors colors={productCardColors(product)} />.

actions?ReactNode

Beside the price, at the foot of the card: a stepper with add to cart, a shopping-list button.

packSize?ReactNode

Under the name: how the product is packed, such as "12 pads × 100 sheets".

children?ReactNode

Under the card's details.

layout?ProductCardLayout

consumer (the default) shows the brand, a discount and the rating. business shows the brand with the article number, the stock level above the price, and the price per unit, marked "excl. VAT" when net, with the first quantity tier.

vatDisplay?VatDisplay

The store's VAT display; by default gross for a consumer and net for a business.

availability?ReactNode

Replaces the availability line, such as with a delivery promise.

pricePlaceholder?ReactNode

Shown instead of the price, such as a sign-in link where the store hides prices from guests.

Any other props go to the root element, like on a plain HTML element.

Change the layout

Pass children to ProductCard to arrange its parts yourself. Every part is exported from @evolve-storefront/feature-product/product-card.

ProductCardAvailabilityWhether the product can be had, as a status tag; data-availability carries the state.
PropTypeDescription
availabilityProductAvailability | null | undefined
ProductCardBrandA consumer card's brand, above the name.
PropTypeDescription
brandstring | null | undefined
ProductCardColorsHow many colors a product comes in, with a swatch per color, for a card's swatches.
PropTypeDescription
colorsreadonly ProductCardColor[]

From productCardColors.

max?number

How many swatches to show before the rest is summed up as "+3".

Any other props go to the root element, like on a plain HTML element.

ProductCardDiscountA discounted price's original amount struck through, the saving in percent, and, given lowest, the lowest price in the 30 days before the discount (EU Omnibus).
PropTypeDescription
moneyMoney | null | undefined
regularMoney | null | undefined
lowest?Money | null
ProductCardImageThe card's image, linked to the product but out of the tab order — the name is the one stop a keyboard makes — with a placeholder of the same shape when there is none.
PropTypeDescription
image{ url: string; alt?: string | null | undefined; } | null | undefined
namestring

Used when the image has no alt text of its own.

hrefstring
loading?"lazy" | "eager"
onLinkClick?MouseEventHandler<HTMLAnchorElement>
renderLink?((props: ProductCardLinkProps) => ReactNode)
renderImage?((props: ProductCardImageRenderProps) => ReactNode)
hoverImage?{ url: string; } | null

Shown over the image while the pointer is on it, such as productCardHoverImage.

children?ReactNode

Over the image: the ribbon, a compare toggle.

Any other props go to the root element, like on a plain HTML element.

ProductCardMetaA business card's line above the name: the brand and the article number.
PropTypeDescription
brandstring | null | undefined
skustring | null | undefined
ProductCardNameThe card's name as a heading, linked to the product.
PropTypeDescription
namestring
hrefstring
headingLevel?HeadingLevel
onLinkClick?MouseEventHandler<HTMLAnchorElement>
renderLink?((props: ProductCardLinkProps) => ReactNode)
ProductCardPriceThe card's price, in its own Price parts.
PropTypeDescription
moneyMoney | null | undefined
localestring
ProductCardRatingThe average rating and how many reviews it comes from.
PropTypeDescription
reviewsListingReviews | null | undefined
ProductCardRibbonThe product's ribbon — "New", "-20%" — its kind as data-kind for a brand to style.
PropTypeDescription
ribbon{ kind?: string | null | undefined; label?: string | null | undefined; } | null | undefined

Any other props go to the root element, like on a plain HTML element.

ProductCardSkuA business card's article number.
PropTypeDescription
skustring | null | undefined
ProductCardStockHow much a business buyer can have, as a status tag toned by level; data-stock carries it.
PropTypeDescription
stockPick<ProductStock, "level" | "lowStockQuantity"> | null | undefined
ProductCardTierHintThe first quantity that lowers the unit price, as "From €8 at 6 pieces".
PropTypeDescription
tiersListingPriceTier[] | null | undefined
baseMoney | null | undefined
vatDisplayVatDisplay

Examples

Vat DisplayThe same card in a store that shows prices including VAT, and in one that shows them excluding VAT, as most B2B stores do: gross first, then net.
<ProductCard product={variant} href={href} vatDisplay="gross" />;
<ProductCard product={variant} href={href} vatDisplay="net" />;
Open in Storybook ↗
ConsumerA consumer's card: the colors it comes in, the brand above the name, the rating, and a discounted price with the original struck through and the saving, including VAT.
<ProductCard
product={variant}
href={href}
layout="consumer"
swatches={<ProductCardColors colors={productCardColors(variant)} />}
hoverImage={productCardHoverImage(variant)}
availability={<DeliveryPromise sku={variant.sku} />}
actions={<WishlistButton sku={variant.sku} />}
/>;
Open in Storybook ↗
BusinessA business buyer's card: the brand and article number above the name, the pack size, the stock level, and at the foot the price excluding VAT per box with the first quantity that lowers it, beside a stepper and the add button.
<ProductCard
product={variant}
href={href}
layout="business"
packSize={packSizeOf(variant)}
actions={
<>
<QuantityStepper value={quantity} onValueChange={setQuantity} label={t("quantity")} … />
<CartAddProductButton sku={variant.sku} quantity={quantity} />
</>
}
/>;
Open in Storybook ↗
Business Stock LevelsA business card's stock as one status tag per level: in stock, low stock with the number, a pre-order and sold out, each in its own tone.
<ProductCard
product={{ ...variant, stock: { level: "LOW_STOCK", lowStockQuantity: 3 } }}
href={href}
layout="business"
/>;
Open in Storybook ↗
Price PlaceholderWhere the store hides prices from guests, the site shows a sign-in link in the price's place.
<ProductCard
product={variant}
href={href}
layout="business"
pricePlaceholder={signedIn ? undefined : <Link href="/sign-in">{t("sign-in-for-price")}</Link>}
/>;
Open in Storybook ↗