Home / Snippets / UI Components /

Product card

E-commerce card with image placeholder, star rating, price, and add-to-cart button.

Wireless Headphones

★★★★ (128)
$79.99 $129.99
Widely Supported
uino-js

Quick implementation

/* HTML:
<div class="product-card">
  <div class="product-card-img"><img src="..." alt="..." /></div>
  <div class="product-card-body">
    <h3 class="product-card-title">...</h3>
    <div class="product-card-rating" aria-label="4 out of 5 stars">...</div>
    <div class="product-card-price">$79.99</div>
    <button class="product-card-btn">Add to cart</button>
  </div>
</div>
*/

.product-card {
  display: flex;
  flex-direction: column;
  width: 18rem;
  border-radius: 1rem;
  border: 1px solid oklch(0.3 0.02 260);
  background: oklch(0.19 0.02 260);
  overflow: hidden;
  transition: box-shadow 0.25s ease, transform 0.25s ease;
}

.product-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 24px oklch(0 0 0 / 0.3);
}

.product-card-img {
  aspect-ratio: 4 / 3;
  overflow: hidden;
}

.product-card-img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.product-card-body {
  padding: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  flex: 1;
}

.product-card-title {
  font-weight: 600;
  font-size: 1rem;
  color: oklch(0.93 0.01 260);
}

.product-card-rating {
  display: flex;
  gap: 0.15rem;
  color: oklch(0.78 0.15 85);
  font-size: 0.85rem;
}

.product-card-price {
  font-weight: 700;
  font-size: 1.25rem;
  color: oklch(0.93 0.01 260);
}

.product-card-btn {
  display: block;
  width: 100%;
  padding: 0.65rem;
  border: none;
  border-radius: 0.5rem;
  background: oklch(0.52 0.22 265);
  color: oklch(1 0 0);
  font-weight: 600;
  font-size: 0.9rem;
  cursor: pointer;
  transition: background 0.2s ease;
  margin-top: auto;
}

.product-card-btn:hover {
  background: oklch(0.45 0.22 265);
}

.product-card-btn:focus-visible {
  outline: 2px solid oklch(0.72 0.19 265);
  outline-offset: 2px;
}

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer building an e-commerce component library.

Goal: A product card with image, title, star rating, price (with optional sale price), and an add-to-cart button — no JavaScript for layout or styling.

Technical constraints:
- Use flexbox column layout with flex: 1 on the body so the button anchors to the bottom.
- Image area uses aspect-ratio: 4/3 with object-fit: cover.
- Star rating uses Unicode ★ characters colored with oklch(0.78 0.15 85).
- Hover lifts the card with translateY(-2px) and adds a box-shadow transition.
- Use oklch() for all color values, not hex or rgba().
- Button uses :focus-visible for keyboard focus ring with outline-offset.

Framework variant (pick one):
A) Vanilla HTML + CSS only.
B) React component — accept image (string), title (string), rating (number), price (number), salePrice (number | null), and onAddToCart handler props.

Edge cases to handle:
- Long product titles should truncate with text-overflow: ellipsis or wrap gracefully.
- Missing image should show a placeholder background, not a broken icon.
- Rating must have an aria-label like "4 out of 5 stars" for screen readers.
- Card must be keyboard-focusable if the entire card is a link (use <a> wrapper).

Return HTML + CSS.

Why this matters in 2026

The product card is the atomic unit of e-commerce UI. Every storefront, marketplace, and comparison site needs one. Modern CSS handles the entire layout — aspect-ratio for consistent image sizing, flex: 1 for bottom-anchored buttons, oklch() for perceptually uniform colors — without the framework-specific wrappers that made these components brittle in the past.

The logic

The card is a flex-direction: column container. The image area uses aspect-ratio: 4/3 to maintain proportions regardless of the actual image dimensions. The body section gets flex: 1 so it expands to fill remaining space, pushing the add-to-cart button to the bottom via margin-top: auto. The hover effect combines translateY(-2px) with a box-shadow transition — both are compositor-friendly and won't trigger layout recalculation.

Accessibility & performance

The star rating needs an aria-label like "4 out of 5 stars" because Unicode stars are not reliably announced by screen readers. If the entire card should be clickable, wrap the title in an <a> and use a pseudo-element stretch technique rather than wrapping the whole card in a link (which creates a massive accessible name). The overflow: hidden on the card clips the image without requiring JavaScript-based lazy loading boundaries, and transform + box-shadow transitions stay on the GPU compositor thread.