Product card system
The most repeated object in the product — it appears on home, shop, category, search, wishlist, cart recommendations, admin and reports. One component, eight designed states, zero variants invented per page.
Anatomy
Every element earns its place by answering a purchase question: what is it, who makes it, do people like it, what does it cost, can I get it, what can I do right now.
- 1Media 4:5 — portrait crops carry apparel better than squares; zooms 4% on hover only (reduced-motion safe: none).
- 2Badges top-left, max two — sale first, then new/best-seller. Stock is never a badge here; it lives in the footer.
- 3Wishlist + quick view top-right — revealed on hover/focus on desktop, always visible on touch. 36px targets.
- 4Quick-add bar — slides up inside the media on hover (desktop only). Touch users add from the PDP; the card never steals their tap.
- 5Brand overline — 11px uppercase muted; doubles as category on brand-less goods.
- 6Name, 2-line clamp — the whole media + name are one link; the card is not a link to avoid nested-interactive traps.
- 7Rating — stars ochre, count muted, value in ink. Hidden entirely until 3+ reviews (no fake 5.0).
- 8Swatches — first four colours + overflow count; selecting a swatch swaps the media tone client-side.
- 9Price stack — current, was (struck), save. Red only when discounted; unit price for grocery/SKU goods.
- 10Stock line — dot + text. Low stock pulses once per session, never loops.
The eight states
Each state below is a separate, named class on the same component. Wishlist and added-to-cart are interactive here — click the heart or Add to cart and watch the choreography.
.apx-skeletonInteraction choreography
| Trigger | Response | Timing |
|---|---|---|
| Hover card | Lift 3px + md shadow + media zoom 4% + action buttons reveal + quick-add slide-up | 200ms ease-out, simultaneous |
| Tap heart | Icon swaps to filled, pop scale 1→1.28→1, toast “Added to wishlist”, header heart count +1 | 340ms spring |
| Add to cart | Button spinner 520ms → card flash 1.1s → badge bump → toast with View cart action | 520 / 1100 / 380ms |
| Swatch tap | Media tone cross-fades, swatch ring moves; price/stock may update if variant differs | 200ms |
| Quick view | Bottom sheet <576px, centred 640px modal above; opens with scale-in, focus to Close | 320ms |
| Out-of-stock tap | Media stays grayscale; “Notify me” opens email capture modal; no dead links, ever | — |
Sizing & grid behaviour
The card is fluid: it never has a fixed width. Columns come from the grid — 2-up on phones (thumb-sized but tap-friendly), 3-up tablet, 4-up desktop, and 4-across rails that snap-scroll below 992px.
Mobile card drops rating + swatches to keep the price above the fold; quick-add hidden, hearts stay.
Implementation specs (Next.js + Bootstrap)
| Element | Class / token | Notes |
|---|---|---|
| Shell | .apx-product | flex column, height:100% so grid rows align |
| Media | .apx-media .apx-media-4x5 | aspect-ratio, object-fit cover, lazy below fold |
| Name link | .apx-product-name a | stretched over media via one <a>, no nested links |
| Wishlist | .apx-product-action[data-wishlist] | aria-pressed, heart ↔ heart-fill |
| Price | .apx-price-current / -was / -save | tabular-nums; <del> semantics on was-price |
| Stock | .apx-stock-in|-low|-out|-backorder | dot + text, never colour alone |
| Skeleton | .apx-skeleton variants | same box metrics as the real card → zero CLS |