Pure TailwindMIT licensed

Tailwind Contained Image Zoom Product Card

The classic CRO zoom: the photo scales inside its frame while a gradient scrim and a 'Quick view' chip fade up over it — and because the frame is the link, the whole thing is one group with a group-focus-visible: twin on every hover state. The zoom is clipped by overflow-hidden, so nothing ever spills into the neighbouring grid cell.

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-03 w-full min-h-svh grid place-items-center bg-tpc03-bg font-tpc03-sans px-5 py-12" aria-label="Contained image zoom product card demo">
  <article class="w-75 max-w-full">
    <a href="#meridian-tote" aria-label="Quick view: Meridian leather tote" class="group/frame relative block aspect-[4/5] rounded-2xl overflow-hidden no-underline transition-shadow duration-400 shadow-[0_14px_34px_-20px_oklch(0.35_0.06_50/0.5)] hover:shadow-[0_22px_48px_-20px_oklch(0.35_0.06_50/0.55)] focus-visible:outline-3 focus-visible:outline-tpc03-acc focus-visible:outline-offset-3">
      <span role="img" aria-label="Product photo: model carrying the Meridian leather tote" class="absolute inset-0 bg-linear-[135deg,var(--color-tpc03-ph),var(--color-tpc03-ph2)] bg-[url('https://images.unsplash.com/photo-1584917865442-de89df76afd3?w=700&h=880&fit=crop&q=70')] bg-cover bg-center transition-transform duration-500 ease-tpc03 group-hover/frame:scale-109 group-focus-visible/frame:scale-109 motion-reduce:transition-none motion-reduce:group-hover/frame:scale-100 motion-reduce:group-focus-visible/frame:scale-100"></span>
      <span class="absolute inset-0 flex items-end justify-center pb-5.5 bg-linear-[0deg,oklch(0.28_0.04_50/0.45),transparent_55%] opacity-0 transition-opacity duration-400 group-hover/frame:opacity-100 group-focus-visible/frame:opacity-100 motion-reduce:transition-none">
        <span class="px-5 py-2.5 rounded-full bg-tpc03-chip text-tpc03-chipink text-[13px] font-semibold tracking-[0.02em] translate-y-2 transition-transform duration-400 ease-tpc03 group-hover/frame:translate-y-0 group-focus-visible/frame:translate-y-0 motion-reduce:transition-none motion-reduce:translate-y-0">Quick view</span>
      </span>
    </a>
    <div class="flex items-baseline justify-between gap-3 pt-4 px-1">
      <h3 class="text-base font-semibold text-tpc03-ink text-pretty">Meridian Leather Tote</h3>
      <p class="text-base font-bold text-tpc03-acc tabular-nums">$240</p>
    </div>
  </article>
</section>
@import "tailwindcss";

@theme {
  --color-tpc03-bg: #f6f3ef;
  --color-tpc03-bg: oklch(0.96 0.008 70);
  --color-tpc03-ink: #2c2115;
  --color-tpc03-ink: oklch(0.28 0.03 60);
  --color-tpc03-ph: #e8ddd0;
  --color-tpc03-ph: oklch(0.91 0.02 70);
  --color-tpc03-ph2: #d8c7b3;
  --color-tpc03-ph2: oklch(0.84 0.035 70);
  --color-tpc03-chip: #fffdfa;
  --color-tpc03-chip: oklch(0.99 0.004 70);
  --color-tpc03-chipink: #3a2a1a;
  --color-tpc03-chipink: oklch(0.3 0.035 55);
  --color-tpc03-acc: #8a5a28;
  --color-tpc03-acc: oklch(0.47 0.09 55);
  --font-tpc03-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --ease-tpc03: cubic-bezier(0.2, 0.7, 0.2, 1);
}

.tpc-03 {
  width: 100%;
  min-height: 100svh;
}

.tpc-03 ::selection {
  background: var(--color-tpc03-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-tpc03-bg: #f6f3ef;
  --color-tpc03-bg: oklch(0.96 0.008 70);
  --color-tpc03-ink: #2c2115;
  --color-tpc03-ink: oklch(0.28 0.03 60);
  --color-tpc03-ph: #e8ddd0;
  --color-tpc03-ph: oklch(0.91 0.02 70);
  --color-tpc03-ph2: #d8c7b3;
  --color-tpc03-ph2: oklch(0.84 0.035 70);
  --color-tpc03-chip: #fffdfa;
  --color-tpc03-chip: oklch(0.99 0.004 70);
  --color-tpc03-chipink: #3a2a1a;
  --color-tpc03-chipink: oklch(0.3 0.035 55);
  --color-tpc03-acc: #8a5a28;
  --color-tpc03-acc: oklch(0.47 0.09 55);
  --font-tpc03-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --ease-tpc03: cubic-bezier(0.2, 0.7, 0.2, 1);
}

.tpc-03 {
  width: 100%;
  min-height: 100svh;
}

.tpc-03 ::selection {
  background: var(--color-tpc03-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&display=swap">

What changed in the port

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

  • Six hover/focus rules in the CSS version become one named group and three responders. The name (/frame) is the port's insurance against nesting this card inside a catalog-level group.
  • The scrim's gradient is a static arbitrary bg-linear-[…] and only opacity transitions. Transitioning the gradient itself would repaint it every frame.
  • group-focus-visible/frame: appears beside every group-hover/frame:. The CSS version repeats :focus-visible in three comma-joined selectors; here the pairing is local and hard to forget.
  • motion-reduce: keeps the scrim and chip and drops only the scale — a reveal that vanishes entirely would hide the affordance.
  • Enhancement over the CSS original: the chip's 8px rise, a frame shadow lift, and the name/price row moved to a baseline-aligned two-column row that survives long product names.
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 Contained Image Zoom Product Card
Source: https://codefronts.com/components/tailwind-css-product-cards/tailwind-contained-image-zoom-product-card/

The classic CRO zoom: the photo scales inside its frame while a gradient scrim and a 'Quick view' chip fade up over it — and because the frame is the link, the whole thing is one group with a group-focus-visible: twin on every hover state. The zoom is clipped by overflow-hidden, so nothing ever spills into the neighbouring grid cell.
## HTML
```html
<section class="tpc-03 w-full min-h-svh grid place-items-center bg-tpc03-bg font-tpc03-sans px-5 py-12" aria-label="Contained image zoom product card demo">
  <article class="w-75 max-w-full">
    <a href="#meridian-tote" aria-label="Quick view: Meridian leather tote" class="group/frame relative block aspect-[4/5] rounded-2xl overflow-hidden no-underline transition-shadow duration-400 shadow-[0_14px_34px_-20px_oklch(0.35_0.06_50/0.5)] hover:shadow-[0_22px_48px_-20px_oklch(0.35_0.06_50/0.55)] focus-visible:outline-3 focus-visible:outline-tpc03-acc focus-visible:outline-offset-3">
      <span role="img" aria-label="Product photo: model carrying the Meridian leather tote" class="absolute inset-0 bg-linear-[135deg,var(--color-tpc03-ph),var(--color-tpc03-ph2)] bg-[url('https://images.unsplash.com/photo-1584917865442-de89df76afd3?w=700&h=880&fit=crop&q=70')] bg-cover bg-center transition-transform duration-500 ease-tpc03 group-hover/frame:scale-109 group-focus-visible/frame:scale-109 motion-reduce:transition-none motion-reduce:group-hover/frame:scale-100 motion-reduce:group-focus-visible/frame:scale-100"></span>
      <span class="absolute inset-0 flex items-end justify-center pb-5.5 bg-linear-[0deg,oklch(0.28_0.04_50/0.45),transparent_55%] opacity-0 transition-opacity duration-400 group-hover/frame:opacity-100 group-focus-visible/frame:opacity-100 motion-reduce:transition-none">
        <span class="px-5 py-2.5 rounded-full bg-tpc03-chip text-tpc03-chipink text-[13px] font-semibold tracking-[0.02em] translate-y-2 transition-transform duration-400 ease-tpc03 group-hover/frame:translate-y-0 group-focus-visible/frame:translate-y-0 motion-reduce:transition-none motion-reduce:translate-y-0">Quick view</span>
      </span>
    </a>
    <div class="flex items-baseline justify-between gap-3 pt-4 px-1">
      <h3 class="text-base font-semibold text-tpc03-ink text-pretty">Meridian Leather Tote</h3>
      <p class="text-base font-bold text-tpc03-acc tabular-nums">$240</p>
    </div>
  </article>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tpc03-bg: #f6f3ef;
  --color-tpc03-bg: oklch(0.96 0.008 70);
  --color-tpc03-ink: #2c2115;
  --color-tpc03-ink: oklch(0.28 0.03 60);
  --color-tpc03-ph: #e8ddd0;
  --color-tpc03-ph: oklch(0.91 0.02 70);
  --color-tpc03-ph2: #d8c7b3;
  --color-tpc03-ph2: oklch(0.84 0.035 70);
  --color-tpc03-chip: #fffdfa;
  --color-tpc03-chip: oklch(0.99 0.004 70);
  --color-tpc03-chipink: #3a2a1a;
  --color-tpc03-chipink: oklch(0.3 0.035 55);
  --color-tpc03-acc: #8a5a28;
  --color-tpc03-acc: oklch(0.47 0.09 55);
  --font-tpc03-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
  --ease-tpc03: cubic-bezier(0.2, 0.7, 0.2, 1);
}

.tpc-03 {
  width: 100%;
  min-height: 100svh;
}

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

How this works

The frame is the interactive element. It is an <a>, not a div with a click handler, and it carries the group:

<a class="group/frame relative block aspect-[4/5] overflow-hidden rounded-2xl">
  <span class="absolute inset-0 bg-cover transition-transform duration-500
               group-hover/frame:scale-109 group-focus-visible/frame:scale-109"></span>

Focus lands on the link, so group-focus-visible/frame: lights the same three layers the mouse does. No extra markup, no tabindex.

Why the image and not the card. Scaling the whole card would scale the type and fight the border radius. Scaling only the absolutely positioned image inside a clipped frame keeps text crisp, keeps the layout box fixed, and keeps the work on the compositor — a catalog of forty of these does not reflow on hover.

The enhancement. The original faded a single scrim. Here the chip also rises 8px, and the frame gains a subtle group-hover/frame:shadow lift — two more utilities for a much more deliberate-feeling reveal.

Make it yours

  • Zoom depth: scale-105 is subtle, scale-109 (here) is the catalog default, scale-115 is a feature-card push.
  • Overlay copy: the chip text — 'Quick view', 'View details', 'Shop the look' — or delete the whole <span> for a pure zoom.
  • Zoom origin: add origin-top or origin-[60%_40%] to push toward a product hotspot.
  • Frame ratio: aspect-[4/5] for fashion, aspect-square for accessories, aspect-[3/4] for footwear.
  • Tempo: duration-500 on the image and duration-400 on the scrim — the slight mismatch is what makes it feel photographic.

Gotchas — read before shipping

  • overflow-hidden on the frame is the whole containment story. Without it the scaled image spills over adjacent cards.
  • Scale the image, not the card: a scaled card scales its text and its radius clips strangely.
  • Give the frame a fixed aspect-* or rows of cards will jog by a few pixels as images of different dimensions load.
  • The chip is decoration over a link, so keep it inside the <a> and out of the accessibility tree's way — the link's aria-label is what gets announced.
  • A named group (group/frame) matters as soon as this card sits inside another group: an unnamed group-hover: matches the nearest group, which may not be the frame.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

transform: scale, overflow clipping and aspect-ratio are universally supported; the floor is Tailwind v4's generated sheet.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…