01 / 18
All 18 in 18 Tailwind CSS Product Cards
- 01Tailwind Product Card with Hover-Reveal Add to Cart
- 02Pure-Tailwind 3D Flip Product Card with Spec Back
- 03Tailwind Contained Image Zoom Product Card
- 04Tailwind Mouse-Tracking 3D Tilt Product Card
- 05Tailwind Slide-Up Details Panel Product Card
- 06Tailwind Minimalist eCommerce Product Card
- 07Tailwind Glassmorphism Product Card
- 08Tailwind Dark-Mode Gadget Product Card with Accent Glow
- 09Tailwind Neon Glowing Conic-Border Product Card
- 10Tailwind Material-Design Elevation Product Card
- 11Tailwind Horizontal Product Card with Container Queries
- 12Tailwind Auto-Fit Product Catalog Grid
- 13Tailwind Sale Ribbon & Coupon Badge Product Card
- 14Tailwind Sneaker Card with Colourway & Size Picker
- 15Tailwind Digital Download Product Card
- 16Tailwind Food Delivery Card with Quantity Stepper
- 17Tailwind Real Estate Listing Card with Favourite Toggle
- 18Tailwind Subscription Pricing Card with Billing Switch
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.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
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><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;
}@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.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@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;
}@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;
}<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">
<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 .xrules collapse into onegroupplusgroup-hover:on the CTA, the media and the shadow. Each responder is now legible in isolation. group-focus-within:is written immediately after everygroup-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)becomesmotion-reduce:variants inline: the CTA is simply always visible, which is the honest fallback for a reveal.- The webfonts load as
<link>tags insetup, not as an@importin the stylesheet: Tailwind emits user@importrules after its own@layerblocks, where the browser drops them. - Enhancement over the CSS original: the 700ms
group-hover:scale-105media push, and a gradient beneath the photo URL so a failed request still reads as a product surface.
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;
}
```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.5fortranslate-x-4(slide in from the side) orscale-95(pop). - Retheme through
--color-tpc01-accand--color-tpc01-acc2in@theme— the fill, the hover fill and the focus ring all read them. - Reveal speed: the
duration-300on the CTA.duration-0gives an instant appear with no transition class churn. - Media push: raise
group-hover:scale-105toscale-110, or delete it for the original's static photo. - Swap
w-75forw-full max-w-75to 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 thegroup-hover:half alone. - Don't reach for
hiddenorgroup-hover:block: display is not animatable and a hidden button is out of the tab order. groupmust 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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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()).