Pure TailwindMIT licensed

Tailwind Justified Image Gallery with Aspect-Ratio Flex Rows

The Google-Images / Flickr look: fluid rows where every photo keeps its native aspect ratio — nothing is cropped — yet each row stretches flush to both edges. No JS measuring pass, and in Tailwind the whole trick reduces to two arbitrary values that both read the same per-item variable.

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-02 w-full min-h-svh grid place-items-center bg-tgl02-bg font-tgl02-sans px-5 py-10" aria-label="Justified aspect-ratio gallery">
  <div class="w-[min(920px,100%)] flex flex-col gap-2">
    <div class="flex gap-2 max-[600px]:flex-wrap">
      <figure style="--r:1.5" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?q=80&w=600&h=400&auto=format&fit=crop" alt="Wide sandy beach" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
      <figure style="--r:0.75" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?q=80&w=450&h=600&auto=format&fit=crop" alt="Coastal lighthouse" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
      <figure style="--r:1.33" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1475721027785-f74eccf877e2?q=80&w=600&h=450&auto=format&fit=crop" alt="Wooden pier over water" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
    </div>
    <div class="flex gap-2 max-[600px]:flex-wrap">
      <figure style="--r:1.78" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1483412033650-1015ddeb83d1?q=80&w=640&h=360&auto=format&fit=crop" alt="Green valley panorama" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
      <figure style="--r:1" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1466692476868-aef1dfb1e735?q=80&w=500&h=500&auto=format&fit=crop" alt="Close-up of a wildflower" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
      <figure style="--r:1.4" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1490645935967-10de6ba17061?q=80&w=560&h=400&auto=format&fit=crop" alt="River through a forest" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
    </div>
    <div class="flex gap-2 max-[600px]:flex-wrap">
      <figure style="--r:0.8" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=480&h=600&auto=format&fit=crop" alt="Tall narrow waterfall" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
      <figure style="--r:1.6" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1502082553048-f009c37129b9?q=80&w=640&h=400&auto=format&fit=crop" alt="Sunlit sand dunes" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
      <figure style="--r:1.2" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1522542550221-31fd19575a2d?q=80&w=540&h=450&auto=format&fit=crop" alt="Alpine meadow in bloom" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tgl02-bg: #fafafa;
  --color-tgl02-bg: oklch(0.98 0 0);
  --color-tgl02-well: #e6e8ea;
  --color-tgl02-well: oklch(0.92 0.004 220);
  --color-tgl02-acc: #2f9e6e;
  --color-tgl02-acc: oklch(0.62 0.13 160);
  --font-tgl02-sans: 'Space Grotesk', system-ui, sans-serif;
}

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

.tgl-02 ::selection {
  background: var(--color-tgl02-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-tgl02-bg: #fafafa;
  --color-tgl02-bg: oklch(0.98 0 0);
  --color-tgl02-well: #e6e8ea;
  --color-tgl02-well: oklch(0.92 0.004 220);
  --color-tgl02-acc: #2f9e6e;
  --color-tgl02-acc: oklch(0.62 0.13 160);
  --font-tgl02-sans: 'Space Grotesk', system-ui, sans-serif;
}

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

.tgl-02 ::selection {
  background: var(--color-tgl02-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.

  • flex: var(--r) 1 0 ports as a single arbitrary value. This is the demo where Tailwind changes the least and reads the best: the two declarations that ARE the technique now sit next to each other on one element.
  • Per-item ratios stay inline style attributes, not @theme entries. They are data about a specific photograph, and every real implementation writes them from image metadata.
  • UI change from the CSS original: the hover zoom gained a motion-reduce:transition-none twin it was missing, and each figure is its own group so the zoom no longer depends on a :hover img descendant selector.
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 Justified Image Gallery with Aspect-Ratio Flex Rows
Source: https://codefronts.com/snippets/tailwind-css-image-gallery/tailwind-justified-image-gallery-aspect-ratio-flex/

The Google-Images / Flickr look: fluid rows where every photo keeps its native aspect ratio — nothing is cropped — yet each row stretches flush to both edges. No JS measuring pass, and in Tailwind the whole trick reduces to two arbitrary values that both read the same per-item variable.
## HTML
```html
<section class="tgl-02 w-full min-h-svh grid place-items-center bg-tgl02-bg font-tgl02-sans px-5 py-10" aria-label="Justified aspect-ratio gallery">
  <div class="w-[min(920px,100%)] flex flex-col gap-2">
    <div class="flex gap-2 max-[600px]:flex-wrap">
      <figure style="--r:1.5" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?q=80&w=600&h=400&auto=format&fit=crop" alt="Wide sandy beach" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
      <figure style="--r:0.75" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?q=80&w=450&h=600&auto=format&fit=crop" alt="Coastal lighthouse" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
      <figure style="--r:1.33" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1475721027785-f74eccf877e2?q=80&w=600&h=450&auto=format&fit=crop" alt="Wooden pier over water" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
    </div>
    <div class="flex gap-2 max-[600px]:flex-wrap">
      <figure style="--r:1.78" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1483412033650-1015ddeb83d1?q=80&w=640&h=360&auto=format&fit=crop" alt="Green valley panorama" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
      <figure style="--r:1" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1466692476868-aef1dfb1e735?q=80&w=500&h=500&auto=format&fit=crop" alt="Close-up of a wildflower" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
      <figure style="--r:1.4" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1490645935967-10de6ba17061?q=80&w=560&h=400&auto=format&fit=crop" alt="River through a forest" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
    </div>
    <div class="flex gap-2 max-[600px]:flex-wrap">
      <figure style="--r:0.8" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=480&h=600&auto=format&fit=crop" alt="Tall narrow waterfall" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
      <figure style="--r:1.6" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1502082553048-f009c37129b9?q=80&w=640&h=400&auto=format&fit=crop" alt="Sunlit sand dunes" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
      <figure style="--r:1.2" class="group flex-[var(--r)_1_0] [aspect-ratio:var(--r)] rounded-lg overflow-hidden bg-tgl02-well m-0 max-[600px]:flex-[1_1_40%]"><img src="https://images.unsplash.com/photo-1522542550221-31fd19575a2d?q=80&w=540&h=450&auto=format&fit=crop" alt="Alpine meadow in bloom" loading="lazy" decoding="async" class="block w-full h-full object-cover transition-transform duration-[400ms] group-hover:scale-105 motion-reduce:transition-none"></figure>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tgl02-bg: #fafafa;
  --color-tgl02-bg: oklch(0.98 0 0);
  --color-tgl02-well: #e6e8ea;
  --color-tgl02-well: oklch(0.92 0.004 220);
  --color-tgl02-acc: #2f9e6e;
  --color-tgl02-acc: oklch(0.62 0.13 160);
  --font-tgl02-sans: 'Space Grotesk', system-ui, sans-serif;
}

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

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

How this works

One variable, two utilities, zero JavaScript. Every item declares its true ratio once:

<figure style="--r:1.5" class="flex-[var(--r)_1_0] [aspect-ratio:var(--r)]">

flex-grow: var(--r) hands each photo a share of the row proportional to its width; flex-basis: 0 makes that share the whole width calculation rather than a bonus on top of an intrinsic size. Because every item in the row then resolves to the same height, the row justifies edge to edge with no cropping — the pure-CSS equivalent of a JS justified-gallery pass.

Rows are explicit, and that is the honest limitation. A JS library reflows items between rows as the viewport changes; CSS cannot. Grouping three photos per row is the trade, and it is the same trade the CSS original made.

Make it yours

  • Add a photo by dropping in a figure with its true --r (width &divide; height); nothing else changes.
  • Control row height indirectly: fewer items per row means a taller row.
  • Generate --r server-side from stored image dimensions for a real feed — it is the one number this layout needs.

Gotchas — read before shipping

  • The --r value must match the real image ratio or the photo letterboxes or crops. object-cover here is a safety net, not the plan.
  • Keep flex-basis: 0 (the _0 in flex-[var(--r)_1_0]). A non-zero basis mixes intrinsic width into the weighting and the justification skews.
  • Arbitrary values cannot contain spaces — flex-[var(--r)_1_0] uses underscores, and Tailwind converts them back.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

aspect-ratio on flex items needs Chrome 88+, Safari 15+, Firefox 89+; the quoted floor is Tailwind v4's generated sheet. Older engines fall back to intrinsic image sizes.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…