Product Card
ProPremium 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.
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
ProUsage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
List row
ProColour swatches
ProPersisting the heart, and putting it back
ProThe 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
ProThere 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
errorto show it in place — without taking away what the user was working on.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| titlerequired | string | — | Product title. |
| pricerequired | number | — | Current price (minor units when `minorUnits` is true). |
| image | string | '' | Product image URL. |
| href | string | '' | Link target for the product. |
| brand | string | '' | Brand / vendor line above the title. |
| compareAt | number | null | null | Compare-at price for a discount. |
| currency | string | 'USD' | ISO 4217 currency code. |
| locale | string | 'en-US' | BCP-47 locale. |
| minorUnits | boolean | false | Treat prices as minor units. |
| rating | number | 0 | Average rating 0..5. |
| reviewCount | number | 0 | Number of reviews (shown next to the stars). |
| badge | string | '' | Corner badge label (e.g. "New", "Sale"). |
| badgeColor | ProductBadgeColor | 'primary' | Corner badge colour. |
| variant | ProductCardVariant | 'grid' | Layout variant. |
| outOfStock | boolean | false | Marks the product unavailable. |
| swatches | readonly ProductSwatch[] | [] | Optional colour swatches. |
| showWishlist | boolean | true | Show the wishlist toggle. |
| showQuickView | boolean | true | Show the quick-view button on hover. |
| showAddToCart | boolean | true | Show the add-to-cart button. |
| addToCartLabel | string | 'Add to cart' | Add-to-cart button label. |
| quickViewLabel | string | 'Quick view' | Quick-view button label. |
| outOfStockLabel | string | 'Out of stock' | Label shown when out of stock. |
| hoverable | boolean | true | Hover elevation. |
| wishlistPending | boolean | false | A wishlist write for this product is in flight. The heart stops responding, so a second toggle cannot race the first and leave `rollback()` ambiguous. |
| addingToCart | boolean | false | An 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. |
| error | SvDataError | string | null | null | The 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. |
| wishlisted | boolean (two-way) | false | Wishlist 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
| Event | Payload type | Description |
|---|---|---|
| addToCart | ProductAddToCartEvent | The shopper asked for this product, and which variant. Persist this. |
| quickView | void | Emitted when the quick-view button is pressed. |
| swatchSelect | ProductSwatch | Emitted when a swatch is selected. |
| wishlistChange | WishlistToggleEvent | The heart moved. Persist it, or call `rollback()`. |
| wishlistedChange | boolean | Emits 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.