Pure TailwindMIT licensed

Tailwind Mosaic Grid Gallery with Contained Zoom on Hover

A crisp mosaic where hovering a tile zooms the photograph cleanly inside its frame: the image scales, the tile does not, so nothing overlaps its neighbours and nothing reflows. The catalog micro-interaction, done the performant way.

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="tgl-13 w-full min-h-svh grid place-items-center bg-tgl13-bg font-tgl13-sans px-5 py-10" aria-label="Mosaic zoom gallery">
  <div class="w-[min(820px,100%)] grid grid-cols-4 gap-2.5 auto-rows-[1fr] [grid-auto-flow:dense] max-[600px]:grid-cols-2">
    <figure class="group relative m-0 col-span-2 row-span-2 [aspect-ratio:auto] overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1451187580459-43490279c0fa?q=80&w=800&h=800&auto=format&fit=crop" alt="Golden hour over rolling hills" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Hills</figcaption>
    </figure>
    <figure class="group relative m-0 aspect-square overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?q=80&w=400&h=400&auto=format&fit=crop" alt="Macro of a flower" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Bloom</figcaption>
    </figure>
    <figure class="group relative m-0 aspect-square overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1519125323398-675f0ddb6308?q=80&w=400&h=400&auto=format&fit=crop" alt="Wild deer in a field" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Wildlife</figcaption>
    </figure>
    <figure class="group relative m-0 aspect-square overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1478720568477-152d9b164e26?q=80&w=400&h=400&auto=format&fit=crop" alt="Backlit green leaf" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Detail</figcaption>
    </figure>
    <figure class="group relative m-0 aspect-square overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1566073771259-6a8506099945?q=80&w=400&h=400&auto=format&fit=crop" alt="Forest waterfall" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Falls</figcaption>
    </figure>
    <figure class="group relative m-0 col-span-2 [aspect-ratio:auto] overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1509440159596-0249088772ff?q=80&w=800&h=400&auto=format&fit=crop" alt="Valley panorama" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Valley</figcaption>
    </figure>
    <figure class="group relative m-0 aspect-square overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1589003077984-894e133dabab?q=80&w=400&h=400&auto=format&fit=crop" alt="Dramatic cloudscape" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Sky</figcaption>
    </figure>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tgl13-bg: #f2f4f1;
  --color-tgl13-bg: oklch(0.96 0.006 145);
  --color-tgl13-well: #dfe4de;
  --color-tgl13-well: oklch(0.91 0.008 145);
  --color-tgl13-acc: #2f8a5a;
  --color-tgl13-acc: oklch(0.56 0.13 155);
  --font-tgl13-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tgl-13 {
  width: 100%;
  min-height: 100svh;
}

.tgl-13 ::selection {
  background: var(--color-tgl13-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-tgl13-bg: #f2f4f1;
  --color-tgl13-bg: oklch(0.96 0.006 145);
  --color-tgl13-well: #dfe4de;
  --color-tgl13-well: oklch(0.91 0.008 145);
  --color-tgl13-acc: #2f8a5a;
  --color-tgl13-acc: oklch(0.56 0.13 155);
  --font-tgl13-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tgl-13 {
  width: 100%;
  min-height: 100svh;
}

.tgl-13 ::selection {
  background: var(--color-tgl13-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=Space+Grotesk:wght@400;500;600;700&display=swap">

What changed in the port

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

  • The clearest demonstration in the collection of what group buys: one state on the tile, two independent reveals on two children, and no descendant selector anywhere.
  • transition-transform rather than transition-all is deliberate. On a grid item, all would put the grid's own resolved sizes in the transition list.
  • UI change from the CSS original: captions now stay hidden until hover on touch as well, but every tile keeps a real <figcaption> in the DOM, so the label is available to assistive tech at all times rather than being a hover-only string.
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 Mosaic Grid Gallery with Contained Zoom on Hover
Source: https://codefronts.com/snippets/tailwind-css-image-gallery/tailwind-mosaic-grid-gallery-contained-zoom/

A crisp mosaic where hovering a tile zooms the photograph cleanly inside its frame: the image scales, the tile does not, so nothing overlaps its neighbours and nothing reflows. The catalog micro-interaction, done the performant way.
## HTML
```html
<section class="tgl-13 w-full min-h-svh grid place-items-center bg-tgl13-bg font-tgl13-sans px-5 py-10" aria-label="Mosaic zoom gallery">
  <div class="w-[min(820px,100%)] grid grid-cols-4 gap-2.5 auto-rows-[1fr] [grid-auto-flow:dense] max-[600px]:grid-cols-2">
    <figure class="group relative m-0 col-span-2 row-span-2 [aspect-ratio:auto] overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1451187580459-43490279c0fa?q=80&w=800&h=800&auto=format&fit=crop" alt="Golden hour over rolling hills" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Hills</figcaption>
    </figure>
    <figure class="group relative m-0 aspect-square overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?q=80&w=400&h=400&auto=format&fit=crop" alt="Macro of a flower" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Bloom</figcaption>
    </figure>
    <figure class="group relative m-0 aspect-square overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1519125323398-675f0ddb6308?q=80&w=400&h=400&auto=format&fit=crop" alt="Wild deer in a field" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Wildlife</figcaption>
    </figure>
    <figure class="group relative m-0 aspect-square overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1478720568477-152d9b164e26?q=80&w=400&h=400&auto=format&fit=crop" alt="Backlit green leaf" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Detail</figcaption>
    </figure>
    <figure class="group relative m-0 aspect-square overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1566073771259-6a8506099945?q=80&w=400&h=400&auto=format&fit=crop" alt="Forest waterfall" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Falls</figcaption>
    </figure>
    <figure class="group relative m-0 col-span-2 [aspect-ratio:auto] overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1509440159596-0249088772ff?q=80&w=800&h=400&auto=format&fit=crop" alt="Valley panorama" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Valley</figcaption>
    </figure>
    <figure class="group relative m-0 aspect-square overflow-hidden rounded-[10px] bg-tgl13-well">
      <img src="https://images.unsplash.com/photo-1589003077984-894e133dabab?q=80&w=400&h=400&auto=format&fit=crop" alt="Dramatic cloudscape" loading="lazy" decoding="async" class="absolute inset-0 w-full h-full object-cover transition-transform duration-[550ms] ease-[cubic-bezier(0.2,0.7,0.2,1)] group-hover:scale-[1.16] group-focus-within:scale-[1.16] motion-reduce:transition-none">
      <figcaption class="absolute inset-x-0 bottom-0 px-3 pt-5 pb-2.5 text-white text-xs font-semibold tracking-[0.04em] bg-[linear-gradient(0deg,rgba(8,20,12,0.8),transparent)] opacity-0 transition-opacity duration-[350ms] group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none">Sky</figcaption>
    </figure>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tgl13-bg: #f2f4f1;
  --color-tgl13-bg: oklch(0.96 0.006 145);
  --color-tgl13-well: #dfe4de;
  --color-tgl13-well: oklch(0.91 0.008 145);
  --color-tgl13-acc: #2f8a5a;
  --color-tgl13-acc: oklch(0.56 0.13 155);
  --font-tgl13-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tgl-13 {
  width: 100%;
  min-height: 100svh;
}

.tgl-13 ::selection {
  background: var(--color-tgl13-acc);
  color: #fff;
}
```

How this works

Scale the image, never the tile. Scaling the tile would push its neighbours and reflow the grid on every hover; scaling the image inside an overflow-hidden frame stays entirely on the compositor. Tailwind makes the division explicit, because the two utilities are on two different elements and you cannot confuse them.

The caption is a second group consumer. One group on the tile drives both the zoom and the scrim caption's fade, so adding a third reveal is one more group-hover: utility, never a new selector.

Feature tiles are declarative. col-span-2 row-span-2 with [aspect-ratio:auto]; every other tile stays aspect-square and the mosaic never loses its grid.

Make it yours

  • Zoom depth: group-hover:scale-[1.16] — 1.08 is restrained, 1.25 is punchy.
  • Focal point: add origin-[60%_30%] to zoom toward the subject rather than the centre.
  • Promote or demote any tile by adding or deleting its span utilities.

Gotchas — read before shipping

  • Without overflow-hidden on the tile the zoomed image spills over its neighbours — the clip is what makes the effect contained.
  • Keep aspect-square on standard tiles so mixed source ratios cannot break the mosaic alignment.
  • Do not transition all: name transition-transform so the grid's own layout properties are never animated by accident.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Grid, transforms and overflow clipping are universal; the floor is Tailwind v4's generated sheet.

Techniques used in this demo

Search CodeFronts

Loading…