SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/product-cards

7 CSS Product Cards

5 Pure CSS2 with JSPublished 2026-10-05Free · no sign-up

Seven product cards for online shops: a card with a quick-add button, a sale card that reads the old and new price correctly, colour swatches made from radio buttons, a cart line with a quantity stepper, hover actions, a rating with a low-stock note and a card for a digital download.

01. Product Card with Quick Add

EcommerceCard

A product card with an image area, name, price and an Add to bag button. The product name is the link, stretched over the card with a pseudo-element, and the button sits above it with z-index, so the card is clickable without nesting interactive elements.

View code
<article class="pc1"><div class="img" aria-hidden="true"></div><h3><a href="#tote">Canvas weekend tote</a></h3><p class="pr">$48</p><button type="button">Add to bag</button></article>
.pc1 { position: relative; display: grid; gap: 4px; width: 230px; max-width: 100%; box-sizing: border-box; padding: 12px; border: 1px solid #e4e7ec; border-radius: 16px; background: #fff; font-family: Inter, system-ui, sans-serif; }
.pc1 .img { aspect-ratio: 1; margin-bottom: 8px; border-radius: 10px; background: linear-gradient(160deg, #f5f5f4, #d6d3d1); }
.pc1 h3 { margin: 0; font-size: 15px; } .pc1 h3 a { color: #101828; text-decoration: none; }
.pc1 h3 a::after { content: ""; position: absolute; inset: 0; border-radius: 16px; }
.pc1 .pr { margin: 0 0 8px; color: #344054; font-weight: 700; }
.pc1 button { position: relative; z-index: 1; padding: 10px; border: 0; border-radius: 10px; background: #101828; color: #fff; font: 700 14px Inter, system-ui, sans-serif; cursor: pointer; }
.pc1:hover { box-shadow: 0 12px 24px -16px rgba(16,24,40,.45); }
.pc1 h3 a:focus-visible { outline: none; }
.pc1 h3 a:focus-visible::after { outline: 3px solid #528bff; outline-offset: 2px; }
.pc1 button:focus-visible { outline: 3px solid #528bff; outline-offset: 2px; }

02. Sale Card with Old Price

EcommerceSale

A sale badge and a struck-through original price. The old price is in a del element and the new one in ins, plus visually hidden words Was and Now, because a line-through alone is not announced by most screen readers.

View code
<article class="pc2"><span class="badge">-25%</span><div class="img" aria-hidden="true"></div><h3>Wool beanie</h3><p class="pr"><del><span class="sr">Was </span>$32</del> <ins><span class="sr">Now </span>$24</ins></p></article>
.pc2 { position: relative; width: 210px; max-width: 100%; box-sizing: border-box; padding: 12px; border-radius: 16px; background: #fff; border: 1px solid #e4e7ec; font-family: Inter, system-ui, sans-serif; }
.pc2 .badge { position: absolute; top: 20px; left: 20px; padding: 4px 8px; border-radius: 6px; background: #b42318; color: #fff; font-weight: 800; font-size: 12px; }
.pc2 .img { aspect-ratio: 4 / 5; border-radius: 10px; background: linear-gradient(170deg, #fee4e2, #fda29b); }
.pc2 h3 { margin: 10px 0 2px; color: #101828; font-size: 15px; }
.pc2 .pr { margin: 0; }
.pc2 del { color: #475467; font-size: 14px; } .pc2 ins { color: #b42318; font-weight: 800; text-decoration: none; }
.pc2 .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }

03. Card with Colour Swatches

EcommerceVariants

Colour swatches under the product are radio buttons, so picking one is keyboard accessible and only one can be chosen. The chosen colour's name is shown in text, and the image area changes colour with :has(:checked).

View code
<article class="pc3"><div class="img" aria-hidden="true"></div><h3>Everyday bottle</h3><fieldset><legend>Colour: <span>Moss</span></legend><label style="--s:#4d7c0f"><input type="radio" name="pc3" value="Moss" checked><span class="sr">Moss</span></label><label style="--s:#1d4ed8"><input type="radio" name="pc3" value="Cobalt"><span class="sr">Cobalt</span></label><label style="--s:#be123c"><input type="radio" name="pc3" value="Berry"><span class="sr">Berry</span></label></fieldset></article>
.pc3 { width: 220px; max-width: 100%; box-sizing: border-box; padding: 12px; border-radius: 16px; background: #151b2e; border: 1px solid #28324f; color: #e2e8f0; font-family: Inter, system-ui, sans-serif; }
.pc3 .img { aspect-ratio: 1; border-radius: 10px; background: radial-gradient(circle at 50% 35%, rgba(255,255,255,.25), transparent 40%), #4d7c0f; transition: background-color .3s; }
.pc3:has([value=Cobalt]:checked) .img { background-color: #1d4ed8; }
.pc3:has([value=Berry]:checked) .img { background-color: #be123c; }
.pc3 h3 { margin: 10px 0 6px; color: #fff; font-size: 15px; }
.pc3 fieldset { display: flex; flex-wrap: wrap; gap: 8px; min-inline-size: 0; margin: 0; padding: 0; border: 0; }
.pc3 legend { width: 100%; margin-bottom: 6px; padding: 0; color: #9aa6c7; font-size: 13px; }
.pc3 legend span { color: #fff; font-weight: 600; }
.pc3 label { position: relative; width: 26px; height: 26px; border-radius: 50%; background: var(--s); cursor: pointer; box-shadow: 0 0 0 2px #151b2e, 0 0 0 3px #3b4466; }
.pc3 input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
.pc3 label:has(:checked) { box-shadow: 0 0 0 2px #151b2e, 0 0 0 4px #fff; }
.pc3 label:has(:focus-visible) { outline: 2px solid #93c5fd; outline-offset: 5px; }
.pc3 .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
var c = document.querySelector('.pc3');
c.addEventListener('change', function (e) { c.querySelector('legend span').textContent = e.target.value; });

04. Cart Line Item with Quantity

EcommerceCart

A product row as it appears in a cart: thumbnail, name, variant, a quantity stepper and the line price. The stepper buttons are labelled Decrease and Increase quantity, and the number is an output element whose change updates the price.

View code
<article class="pc4"><div class="img" aria-hidden="true"></div><div class="i"><h3>Ceramic pour-over</h3><p>White · 600 ml</p><div class="q"><button type="button" aria-label="Decrease quantity">−</button><output aria-label="Quantity">1</output><button type="button" aria-label="Increase quantity">+</button></div></div><p class="pr">$36</p></article>
.pc4 { display: grid; grid-template-columns: 72px 1fr auto; gap: 12px; align-items: start; width: 340px; max-width: 100%; box-sizing: border-box; padding: 12px; border: 1px solid #e4e7ec; border-radius: 14px; background: #fff; font-family: Inter, system-ui, sans-serif; color: #101828; }
.pc4 .img { aspect-ratio: 1; border-radius: 10px; background: linear-gradient(150deg, #f8fafc, #cbd5e1); }
.pc4 h3 { margin: 0; font-size: 14px; } .pc4 .i p { margin: 2px 0 8px; color: #475467; font-size: 13px; }
.pc4 .i { min-width: 0; }
.pc4 .q { display: inline-flex; align-items: center; border: 1px solid #d0d5dd; border-radius: 8px; }
.pc4 .q button { width: 30px; height: 30px; border: 0; background: transparent; font-size: 16px; cursor: pointer; color: #344054; }
.pc4 output { min-width: 24px; text-align: center; font-weight: 700; font-size: 14px; }
.pc4 .pr { margin: 0; font-weight: 800; }
.pc4 button:focus-visible { outline: 2px solid #528bff; outline-offset: 1px; border-radius: 6px; }
var o = document.querySelector('.pc4 output'), pr = document.querySelector('.pc4 .pr'), b = document.querySelectorAll('.pc4 .q button');
function set(n) { n = Math.max(1, Math.min(9, n)); o.value = n; pr.textContent = '$' + n * 36; }
b[0].onclick = function () { set(+o.value - 1); }; b[1].onclick = function () { set(+o.value + 1); };

05. Card with Hover Actions

EcommerceHover

Quick view and wishlist buttons slide up over the image on hover. They also appear when anything inside the card has focus, through :focus-within, and they are always visible on touch screens where hover does not exist.

View code
<article class="pc5"><div class="img"><div class="acts"><button type="button">Quick view</button><button type="button" aria-label="Save to wishlist">♡</button></div></div><h3><a href="#jacket">Waxed field jacket</a></h3><p>$145</p></article>
.pc5 { width: 220px; max-width: 100%; font-family: Inter, system-ui, sans-serif; }
.pc5 .img { position: relative; aspect-ratio: 3 / 4; border-radius: 14px; overflow: hidden; background: linear-gradient(160deg, #3f3f46, #18181b); }
.pc5 .acts { position: absolute; left: 10px; right: 10px; bottom: 10px; display: flex; gap: 8px; transform: translateY(130%); transition: transform .3s ease; }
.pc5:hover .acts, .pc5:focus-within .acts { transform: none; }
.pc5 .acts button { padding: 10px; border: 0; border-radius: 10px; background: #fff; color: #18181b; font: 700 13px Inter, system-ui, sans-serif; cursor: pointer; }
.pc5 .acts button:first-child { flex: 1; }
.pc5 .acts button:last-child { width: 40px; font-size: 18px; }
.pc5 h3 { margin: 10px 0 2px; font-size: 15px; } .pc5 h3 a { color: #f4f4f5; text-decoration: none; }
.pc5 p { margin: 0; color: #a1a1aa; font-weight: 700; }
.pc5 :focus-visible { outline: 2px solid #a5b4fc; outline-offset: 2px; }
@media (hover: none) { .pc5 .acts { transform: none; } }
@media (prefers-reduced-motion: reduce) { .pc5 .acts { transition: none; } }

06. Card with Rating and Stock

EcommerceRating

Adds the details shoppers scan for: a star rating with its count and a low-stock note. Partial stars are drawn by clipping a row of gold stars to the rating's width, and the rating is also given as text for screen readers.

View code
<article class="pc6"><div class="img" aria-hidden="true"></div><h3>Trail running sock</h3><p class="rt"><span class="stars" style="--v:4.3" aria-hidden="true"></span><span class="sr">Rated 4.3 out of 5</span> <span>(128)</span></p><p class="pr">$14</p><p class="st">Only 4 left in your size</p></article>
.pc6 { width: 210px; max-width: 100%; box-sizing: border-box; padding: 12px; border: 1px solid #e4e7ec; border-radius: 16px; background: #fff; font-family: Inter, system-ui, sans-serif; color: #101828; }
.pc6 .img { aspect-ratio: 1; border-radius: 10px; background: linear-gradient(150deg, #ecfdf3, #a6f4c5); }
.pc6 h3 { margin: 10px 0 4px; font-size: 15px; }
.pc6 .rt { display: flex; align-items: center; gap: 4px; margin: 0; color: #475467; font-size: 13px; }
.pc6 .stars { position: relative; font-size: 14px; letter-spacing: 1px; line-height: 1; }
.pc6 .stars::before { content: "★★★★★"; color: #d0d5dd; }
.pc6 .stars::after { content: "★★★★★"; position: absolute; left: 0; top: 0; width: calc(var(--v) / 5 * 100%); overflow: hidden; color: #f79009; white-space: nowrap; }
.pc6 .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.pc6 .pr { margin: 6px 0 2px; font-weight: 800; }
.pc6 .st { margin: 0; color: #b54708; font-size: 13px; font-weight: 600; }

07. Digital Product Card

EcommerceDigital

A card for a template or e-book with a file type tag, what is included as a checklist and a single price button. The checklist uses real list markup with tick marks drawn in CSS, so the items are still a list for screen readers.

View code
<article class="pc7"><p class="tag">Figma + HTML</p><h3>Storefront UI kit</h3><ul><li>48 page templates</li><li>Light and dark themes</li><li>Free updates for a year</li></ul><button type="button">Buy for $59</button></article>
.pc7 { width: 250px; max-width: 100%; box-sizing: border-box; padding: 20px; border-radius: 18px; background: linear-gradient(170deg, #1e1b4b, #111827); border: 1px solid #312e81; color: #c7d2fe; font-family: Inter, system-ui, sans-serif; }
.pc7 .tag { display: inline-block; margin: 0 0 10px; padding: 3px 8px; border-radius: 6px; background: #312e81; color: #e0e7ff; font: 600 12px "JetBrains Mono", ui-monospace, monospace; }
.pc7 h3 { margin: 0 0 10px; color: #fff; font-size: 19px; }
.pc7 ul { margin: 0 0 16px; padding: 0; list-style: none; display: grid; gap: 6px; font-size: 14px; }
.pc7 li { display: flex; gap: 8px; } .pc7 li::before { content: ""; flex: none; width: 10px; height: 5px; margin-top: 5px; border-left: 2px solid #34d399; border-bottom: 2px solid #34d399; transform: rotate(-45deg); }
.pc7 button { width: 100%; padding: 12px; border: 0; border-radius: 10px; background: #6366f1; color: #fff; font: 700 15px Inter, system-ui, sans-serif; cursor: pointer; }
.pc7 button:focus-visible { outline: 3px solid #c7d2fe; outline-offset: 2px; }

About these CSS Product Cards

One link, plus real buttons

Make the product name the link and stretch it over the card with a pseudo-element. Buttons such as Add to bag sit above it with position: relative and a z-index, so you never nest a button inside a link.

Prices that read correctly

A struck-through price is usually not announced. Put the old price in <del> and the new one in <ins>, and add hidden words such as Was and Now so the meaning survives without the styling.

Variants as radio buttons

Colour and size choices are a single choice from a set, which is exactly a radio group. Hide the circle, style the label as a swatch, and use :has(:checked) to show the selection.

Images and layout shift

Give the image area a fixed aspect-ratio so the card does not jump when the photo loads. In real cards use width and height attributes and loading="lazy" on product images below the fold.

Frequently asked questions

Where are the product photos?

The demos use CSS gradients as stand-ins so they load instantly. Swap in your own img elements with alt text.

Do these work with Shopify?

Yes. The HTML maps onto a Liquid product card snippet; keep the class names and loop over your products.

How many cards per row?

Use a grid with repeat(auto-fill, minmax(200px, 1fr)) and the row count adapts to the screen.