Product Card

Pro

Premium commerce product card composing price and rating with a wishlist toggle, badge, colour swatches, add-to-cart and an out-of-stock state. Grid and list layouts.

</>sv-product-card productcardecommerceshopwishlist

Complexity tier: Primitive Presents a value: held to semantic markup and contrast, with its right-to-left and print gaps recorded and the right-to-left debt allowed only to shrink.

import { SvProductCardComponent } from '@styloviz-pro/commerce';

Demo & usage

Basic usage

Pro

Usage example — open the gallery for the full interactive demo.

Pro

An interactive demo for this component lives in the gallery.

Open live demo

More examples

List row

Pro

Colour swatches

Pro

Persisting the heart, and putting it back

Pro

The heart flips the instant it is pressed — a heart that waits for a round trip reads as a broken button. `rollback()` is on the event because the code that learns the write failed is the code handling it. It refuses once the heart has been toggled again, so keep `wishlistPending` bound.

Adding the right variant, once

Pro

There is no `rollback()` on `addToCart` and inventing one would be a lie — the card holds no cart, so it changes nothing optimistically. What it gives you instead is the swatch in effect and a `requestKey`: bind `addingToCart` so one press cannot become two, and de-duplicate on the key so a retry cannot either.

When to use

  • Product listing grids, search results, wishlists, related-product rails and comparison pickers.
  • You want price, rating and a wishlist affordance in one consistent card.
  • For dense non-visual rows, prefer a List View instead.

Async states

  • error

Server writes

Writes
  • addToCart
  • wishlistChange

Each payload carries a requestKey — send it with the request so a retry cannot apply the same change twice.

Rollback
Call event.rollback() when the save fails — the change is already on screen, and this is what puts it back.
While saving
Bind wishlistPending — with two writes outstanding there is no single state to roll back to, so the action stays locked until the first one lands.
On failure
Pass the failure to error to show it in place — without taking away what the user was working on.

Inputs

NameTypeDefaultDescription
titlerequiredstring—Product title.
pricerequirednumber—Current price (minor units when `minorUnits` is true).
imagestring''Product image URL.
hrefstring''Link target for the product.
brandstring''Brand / vendor line above the title.
compareAtnumber | nullnullCompare-at price for a discount.
currencystring'USD'ISO 4217 currency code.
localestring'en-US'BCP-47 locale.
minorUnitsbooleanfalseTreat prices as minor units.
ratingnumber0Average rating 0..5.
reviewCountnumber0Number of reviews (shown next to the stars).
badgestring''Corner badge label (e.g. "New", "Sale").
badgeColorProductBadgeColor'primary'Corner badge colour.
variantProductCardVariant'grid'Layout variant.
outOfStockbooleanfalseMarks the product unavailable.
swatchesreadonly ProductSwatch[][]Optional colour swatches.
showWishlistbooleantrueShow the wishlist toggle.
showQuickViewbooleantrueShow the quick-view button on hover.
showAddToCartbooleantrueShow the add-to-cart button.
addToCartLabelstring'Add to cart'Add-to-cart button label.
quickViewLabelstring'Quick view'Quick-view button label.
outOfStockLabelstring'Out of stock'Label shown when out of stock.
hoverablebooleantrueHover elevation.
wishlistPendingbooleanfalseA wishlist write for this product is in flight. The heart stops responding, so a second toggle cannot race the first and leave `rollback()` ambiguous.
addingToCartbooleanfalseAn add-to-cart write for this product is in flight. The button stops responding — the same shopper pressing it twice is how a cart ends up with two of something.
errorSvDataError | string | nullnullThe last write for this card failed. Rendered as a danger note by the price. One input covers both actions because the HOST supplies the wording, so "Could not save to your wishlist" already says which one failed. Correcting the heart is `rollback()`'s job, not this input's.
wishlistedboolean (two-way)falseWishlist state (two-way: `[(wishlisted)]`). Bind it two-way, not one-way. The card flips the heart itself, and being a `model` is what pushes that back into YOUR signal, so the two never drift. Over a plain input it would: Angular skips an input binding whose expression has not changed, so re-asserting `false` against a heart already showing true is ignored and a rejected toggle stays on screen for good.

Outputs

EventPayload typeDescription
addToCartProductAddToCartEventThe shopper asked for this product, and which variant. Persist this.
quickViewvoidEmitted when the quick-view button is pressed.
swatchSelectProductSwatchEmitted when a swatch is selected.
wishlistChangeWishlistToggleEventThe heart moved. Persist it, or call `rollback()`.
wishlistedChangebooleanEmits the new `wishlisted` whenever it changes — the output half of `[(wishlisted)]`.

Customization & theming

  • Grid and list layouts.
  • Corner badge with five colours.
  • Colour swatches with a selection output.
  • Toggle the wishlist, add-to-cart and out-of-stock affordances.
  • `error` renders next to the price rather than over the image, which already carries the badge, the heart and the out-of-stock banner.

Best practices

  • Do: Set `href` so the whole card is a keyboard-native link; actions stay independently clickable above it.
  • Do: Keep image aspect ratios uniform across a grid so cards align.
  • Tip: Bind `[(wishlisted)]` to your wishlist store so the heart reflects saved state.
  • Do: Bind `wishlistPending` and `addingToCart` while the matching write is in flight. They are what make `rollback()` unambiguous and what stops one press from adding two of something.
  • Do: Call `event.rollback()` when a wishlist write fails, and put the reason in `error`. The input reports the failure; it does not undo it.
  • Do: Send `event.requestKey` with the write and ignore a key you have already applied, so a retried request cannot charge twice.
  • Do: Read `event.swatch` on `addToCart` rather than tracking `swatchSelect` separately — the event already knows which variant is in effect.
  • Don't: Do not hold the heart still until the server answers. The delay reads as a dead button, which is exactly why the card moves first and hands back a way out.
  • Don't: Do not keep a `rollback()` and call it later. It refuses once the heart has been toggled again, because restoring that snapshot would revert the newer toggle too.

Accessibility

  • Wishlist toggle exposes `aria-pressed` and a descriptive label.
  • Stretched title link keeps the whole card clickable while swatches and buttons stay reachable.
  • Add-to-cart is disabled and dimmed when out of stock.
  • Both actions set `aria-busy` while their write is in flight, so the wait is announced rather than only animated.
  • The failure note is a live `role="alert"`, so a rejected write is read out rather than silently reverting the heart.
  • The pending pulse is `motion-safe:`, so it is absent under `prefers-reduced-motion`.

Feedback on Product Card

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.