Pure TailwindMIT licensed

Tailwind Product Card with Hover-Reveal Add to Cart

The most-searched hover pattern in Tier-1 e-commerce, ported to variants: the card stays calm and type-led at rest, and the 'Add to Cart' button rises into place on hover — or on group-focus-within:, which is the part most implementations forget. One group on the card drives the CTA, the shadow lift and a slow media zoom, so all three read off the same state.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tpc-01 w-full min-h-svh grid place-items-center bg-tpc01-bg font-tpc01-sans px-5 py-12" aria-label="Hover-reveal add to cart product card demo">
  <article class="group w-75 max-w-full rounded-[18px] overflow-hidden bg-tpc01-card shadow-[0_1px_2px_oklch(0.25_0.03_260/0.06),0_12px_32px_-18px_oklch(0.25_0.03_260/0.28)] transition-shadow duration-300 hover:shadow-[0_1px_2px_oklch(0.25_0.03_260/0.06),0_26px_50px_-22px_oklch(0.25_0.03_260/0.4)] focus-within:shadow-[0_1px_2px_oklch(0.25_0.03_260/0.06),0_26px_50px_-22px_oklch(0.25_0.03_260/0.4)]">
    <div class="aspect-square overflow-hidden bg-linear-[135deg,var(--color-tpc01-ph),var(--color-tpc01-ph2)]">
      <div role="img" aria-label="Product photo: Aurora wireless headphones" class="w-full h-full bg-[url('https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=700&h=700&fit=crop&q=70')] bg-cover bg-center transition-transform duration-700 ease-tpc01 group-hover:scale-105 motion-reduce:transition-none motion-reduce:group-hover:scale-100"></div>
    </div>
    <div class="flex flex-col gap-1 px-5 pt-5 pb-5.5">
      <p class="font-tpc01-mono text-[11px] tracking-[0.18em] uppercase text-tpc01-dim">SoundField</p>
      <h3 class="text-[17px] font-semibold leading-[1.3] text-tpc01-ink text-pretty">Aurora Wireless Headphones</h3>
      <p class="mt-0.5 text-[18px] font-bold text-tpc01-ink tabular-nums">$149.00</p>
      <button type="button" class="mt-3.5 min-h-11 h-11.5 rounded-xl border-0 bg-tpc01-acc text-tpc01-onacc text-[15px] font-semibold tracking-[0.01em] cursor-pointer opacity-0 translate-y-3.5 transition-[opacity,translate,background-color] duration-300 ease-tpc01 group-hover:opacity-100 group-hover:translate-y-0 group-focus-within:opacity-100 group-focus-within:translate-y-0 hover:bg-tpc01-acc2 focus-visible:outline-3 focus-visible:outline-tpc01-ring focus-visible:outline-offset-2 motion-reduce:opacity-100 motion-reduce:translate-y-0 motion-reduce:transition-none">Add to Cart</button>
    </div>
  </article>
</section>
@import "tailwindcss";

@theme {
  --color-tpc01-bg: #f4f5f7;
  --color-tpc01-bg: oklch(0.97 0.004 265);
  --color-tpc01-card: #ffffff;
  --color-tpc01-card: oklch(1 0 0);
  --color-tpc01-ink: #101828;
  --color-tpc01-ink: oklch(0.23 0.03 265);
  --color-tpc01-dim: #98a2b3;
  --color-tpc01-dim: oklch(0.7 0.02 265);
  --color-tpc01-ph: #e9edf3;
  --color-tpc01-ph: oklch(0.93 0.01 265);
  --color-tpc01-ph2: #dfe4ec;
  --color-tpc01-ph2: oklch(0.9 0.015 265);
  --color-tpc01-acc: #3a5ccc;
  --color-tpc01-acc: oklch(0.55 0.16 265);
  --color-tpc01-acc2: #2f4cab;
  --color-tpc01-acc2: oklch(0.48 0.15 265);
  --color-tpc01-onacc: #ffffff;
  --color-tpc01-onacc: oklch(1 0 0);
  --color-tpc01-ring: #9db0ea;
  --color-tpc01-ring: oklch(0.75 0.09 265);
  --font-tpc01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-tpc01-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-tpc01: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the stage is sized correctly in the moment before the
   browser build has compiled its utilities. */

.tpc-01 {
  width: 100%;
  min-height: 100svh;
}
/* The one rule with no variant form: a scoped ::selection. */

.tpc-01 ::selection {
  background: var(--color-tpc01-acc);
  color: #fff;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tpc01-bg: #f4f5f7;
  --color-tpc01-bg: oklch(0.97 0.004 265);
  --color-tpc01-card: #ffffff;
  --color-tpc01-card: oklch(1 0 0);
  --color-tpc01-ink: #101828;
  --color-tpc01-ink: oklch(0.23 0.03 265);
  --color-tpc01-dim: #98a2b3;
  --color-tpc01-dim: oklch(0.7 0.02 265);
  --color-tpc01-ph: #e9edf3;
  --color-tpc01-ph: oklch(0.93 0.01 265);
  --color-tpc01-ph2: #dfe4ec;
  --color-tpc01-ph2: oklch(0.9 0.015 265);
  --color-tpc01-acc: #3a5ccc;
  --color-tpc01-acc: oklch(0.55 0.16 265);
  --color-tpc01-acc2: #2f4cab;
  --color-tpc01-acc2: oklch(0.48 0.15 265);
  --color-tpc01-onacc: #ffffff;
  --color-tpc01-onacc: oklch(1 0 0);
  --color-tpc01-ring: #9db0ea;
  --color-tpc01-ring: oklch(0.75 0.09 265);
  --font-tpc01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-tpc01-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-tpc01: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the stage is sized correctly in the moment before the
   browser build has compiled its utilities. */

.tpc-01 {
  width: 100%;
  min-height: 100svh;
}
/* The one rule with no variant form: a scoped ::selection. */

.tpc-01 ::selection {
  background: var(--color-tpc01-acc);
  color: #fff;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • Three .cpc-01__card:hover .x rules collapse into one group plus group-hover: on the CTA, the media and the shadow. Each responder is now legible in isolation.
  • group-focus-within: is written immediately after every group-hover: pair. In the CSS version those live in a comma-joined selector that is easy to forget to extend; here forgetting is visible on the element.
  • The two-layer shadow stays an arbitrary value. A key+ambient pair is a single design decision, and splitting it across shadow-* plus a ring utility would let the layers drift.
  • @media (prefers-reduced-motion) becomes motion-reduce: variants inline: the CTA is simply always visible, which is the honest fallback for a reveal.
  • The webfonts load as <link> tags in setup, not as an @import in the stylesheet: Tailwind emits user @import rules after its own @layer blocks, where the browser drops them.
  • Enhancement over the CSS original: the 700ms group-hover:scale-105 media push, and a gradient beneath the photo URL so a failed request still reads as a product surface.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Product Card with Hover-Reveal Add to Cart
Source: https://codefronts.com/components/tailwind-css-product-cards/tailwind-product-card-hover-reveal-add-to-cart/

The most-searched hover pattern in Tier-1 e-commerce, ported to variants: the card stays calm and type-led at rest, and the 'Add to Cart' button rises into place on hover — or on group-focus-within:, which is the part most implementations forget. One group on the card drives the CTA, the shadow lift and a slow media zoom, so all three read off the same state.
## HTML
```html
<section class="tpc-01 w-full min-h-svh grid place-items-center bg-tpc01-bg font-tpc01-sans px-5 py-12" aria-label="Hover-reveal add to cart product card demo">
  <article class="group w-75 max-w-full rounded-[18px] overflow-hidden bg-tpc01-card shadow-[0_1px_2px_oklch(0.25_0.03_260/0.06),0_12px_32px_-18px_oklch(0.25_0.03_260/0.28)] transition-shadow duration-300 hover:shadow-[0_1px_2px_oklch(0.25_0.03_260/0.06),0_26px_50px_-22px_oklch(0.25_0.03_260/0.4)] focus-within:shadow-[0_1px_2px_oklch(0.25_0.03_260/0.06),0_26px_50px_-22px_oklch(0.25_0.03_260/0.4)]">
    <div class="aspect-square overflow-hidden bg-linear-[135deg,var(--color-tpc01-ph),var(--color-tpc01-ph2)]">
      <div role="img" aria-label="Product photo: Aurora wireless headphones" class="w-full h-full bg-[url('https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=700&h=700&fit=crop&q=70')] bg-cover bg-center transition-transform duration-700 ease-tpc01 group-hover:scale-105 motion-reduce:transition-none motion-reduce:group-hover:scale-100"></div>
    </div>
    <div class="flex flex-col gap-1 px-5 pt-5 pb-5.5">
      <p class="font-tpc01-mono text-[11px] tracking-[0.18em] uppercase text-tpc01-dim">SoundField</p>
      <h3 class="text-[17px] font-semibold leading-[1.3] text-tpc01-ink text-pretty">Aurora Wireless Headphones</h3>
      <p class="mt-0.5 text-[18px] font-bold text-tpc01-ink tabular-nums">$149.00</p>
      <button type="button" class="mt-3.5 min-h-11 h-11.5 rounded-xl border-0 bg-tpc01-acc text-tpc01-onacc text-[15px] font-semibold tracking-[0.01em] cursor-pointer opacity-0 translate-y-3.5 transition-[opacity,translate,background-color] duration-300 ease-tpc01 group-hover:opacity-100 group-hover:translate-y-0 group-focus-within:opacity-100 group-focus-within:translate-y-0 hover:bg-tpc01-acc2 focus-visible:outline-3 focus-visible:outline-tpc01-ring focus-visible:outline-offset-2 motion-reduce:opacity-100 motion-reduce:translate-y-0 motion-reduce:transition-none">Add to Cart</button>
    </div>
  </article>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tpc01-bg: #f4f5f7;
  --color-tpc01-bg: oklch(0.97 0.004 265);
  --color-tpc01-card: #ffffff;
  --color-tpc01-card: oklch(1 0 0);
  --color-tpc01-ink: #101828;
  --color-tpc01-ink: oklch(0.23 0.03 265);
  --color-tpc01-dim: #98a2b3;
  --color-tpc01-dim: oklch(0.7 0.02 265);
  --color-tpc01-ph: #e9edf3;
  --color-tpc01-ph: oklch(0.93 0.01 265);
  --color-tpc01-ph2: #dfe4ec;
  --color-tpc01-ph2: oklch(0.9 0.015 265);
  --color-tpc01-acc: #3a5ccc;
  --color-tpc01-acc: oklch(0.55 0.16 265);
  --color-tpc01-acc2: #2f4cab;
  --color-tpc01-acc2: oklch(0.48 0.15 265);
  --color-tpc01-onacc: #ffffff;
  --color-tpc01-onacc: oklch(1 0 0);
  --color-tpc01-ring: #9db0ea;
  --color-tpc01-ring: oklch(0.75 0.09 265);
  --font-tpc01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-tpc01-mono: 'JetBrains Mono', ui-monospace, monospace;
  --ease-tpc01: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the stage is sized correctly in the moment before the
   browser build has compiled its utilities. */

.tpc-01 {
  width: 100%;
  min-height: 100svh;
}
/* The one rule with no variant form: a scoped ::selection. */

.tpc-01 ::selection {
  background: var(--color-tpc01-acc);
  color: #fff;
}
```

How this works

One group, three responders. The CSS version writes three separate .cpc-01__card:hover .x rules. In utilities the card is marked once and each responder states its own answer:

<div class="group …">
  <div class="… transition-transform duration-700 group-hover:scale-105"></div>
  <button class="opacity-0 translate-y-3.5
    group-hover:opacity-100 group-hover:translate-y-0
    group-focus-within:opacity-100 group-focus-within:translate-y-0">Add to Cart</button>
</div>

Why the button stays in flow. It is opacity-0, not hidden: the card's height is identical at rest and on hover, so a grid of these never reflows, and the button never leaves the tab order. That is also what makes group-focus-within: work — a tab into the card reveals the very button that received focus.

The enhancement. The original left the photo static. Here the media carries group-hover:scale-105 at 700ms inside an overflow-hidden frame — slow enough to read as a camera push rather than a UI animation, and clipped by the card's own radius.

Make it yours

  • Reveal direction: swap translate-y-3.5 for translate-x-4 (slide in from the side) or scale-95 (pop).
  • Retheme through --color-tpc01-acc and --color-tpc01-acc2 in @theme — the fill, the hover fill and the focus ring all read them.
  • Reveal speed: the duration-300 on the CTA. duration-0 gives an instant appear with no transition class churn.
  • Media push: raise group-hover:scale-105 to scale-110, or delete it for the original's static photo.
  • Swap w-75 for w-full max-w-75 to drop the card straight into a catalog grid cell.

Gotchas — read before shipping

  • A hover-only CTA is invisible on touch. The group-focus-within: twin and the in-flow button are what keep it reachable — never ship the group-hover: half alone.
  • Don't reach for hidden or group-hover:block: display is not animatable and a hidden button is out of the tab order.
  • group must sit on the card, not the section — group-hover: matches the nearest marked ancestor, and a section-level group would fire from anywhere on the stage.
  • Arbitrary shadows cannot contain spaces: every space inside shadow-[…] becomes an underscore, and the comma between layers stays a comma.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:focus-within and transform/opacity transitions are universally supported; the floor quoted here is Tailwind v4's own generated sheet (cascade layers, @property, color-mix()).

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…