Pure TailwindMIT licensed

Tailwind Hero Photo Collage Grid

A landing hero with narrative copy on the left and a five-tile asymmetric photo collage on the right. Every tile states its own placement — col-start-1 col-span-2 row-start-1 row-span-2 — so the collage rhythm is readable in the markup, and the readability overlay every photo grid ships is one after: chain instead of a pseudo-element block.

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="tcg-12 w-full min-h-svh flex items-center justify-center bg-tcg12-bg font-tcg12-sans text-tcg12-ink px-[clamp(20px,3vw,48px)] py-[clamp(28px,4vw,56px)]" aria-label="Hero photo collage grid demo">
  <div class="w-full max-w-[1200px] grid [grid-template-columns:1fr_1.2fr] max-[900px]:[grid-template-columns:1fr] items-center gap-[clamp(24px,3.4vw,56px)] max-[900px]:gap-8">

    <div class="flex flex-col gap-4.5 max-w-[520px] max-[900px]:max-w-none max-[900px]:items-center max-[900px]:text-center">
      <span class="w-fit px-3 py-1.5 rounded-full text-[.72rem] font-bold tracking-[0.16em] uppercase text-tcg12-accent bg-tcg12-accentsoft">New &middot; 2026 collection</span>
      <h2 class="text-[clamp(2.2rem,4.5vw,3.6rem)] font-black tracking-[-0.03em] leading-[1.02]">Stay places that<br><em class="font-tcg12-serif italic font-bold text-tcg12-accent">feel like home.</em></h2>
      <p class="max-w-[46ch] text-[clamp(.96rem,1.2vw,1.1rem)] leading-[1.55] text-tcg12-muted text-pretty">Hand-picked stays in 47 cities. Verified by humans. Booked by people who care where they sleep.</p>
      <div class="mt-1.5 flex flex-wrap items-center gap-4">
        <a href="#stays" class="inline-flex items-center min-h-11 px-5.5 py-3.5 rounded-[10px] bg-tcg12-ink text-white text-[.96rem] font-bold no-underline transition-[background,translate] duration-200 hover:bg-tcg12-accent hover:-translate-y-0.5 focus-visible:outline-2 focus-visible:outline-tcg12-accent focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:hover:translate-y-0">Browse stays &rarr;</a>
        <a href="#how" class="inline-flex items-center min-h-11 text-[.94rem] font-semibold text-tcg12-ink underline decoration-1 underline-offset-4 hover:text-tcg12-accent focus-visible:outline-2 focus-visible:outline-tcg12-accent focus-visible:outline-offset-3">How it works</a>
      </div>
      <dl class="mt-4 pt-6 flex gap-[clamp(20px,3vw,40px)] max-[560px]:gap-3.5 border-t border-tcg12-line">
        <div class="flex flex-col gap-0.5">
          <dd class="text-2xl max-[560px]:text-xl font-black tracking-[-0.02em] tabular-nums">4.9 &#9733;</dd>
          <dt class="text-[.78rem] font-semibold text-tcg12-muted">Avg rating</dt>
        </div>
        <div class="flex flex-col gap-0.5">
          <dd class="text-2xl max-[560px]:text-xl font-black tracking-[-0.02em] tabular-nums">12K+</dd>
          <dt class="text-[.78rem] font-semibold text-tcg12-muted">Verified stays</dt>
        </div>
        <div class="flex flex-col gap-0.5">
          <dd class="text-2xl max-[560px]:text-xl font-black tracking-[-0.02em] tabular-nums">47</dd>
          <dt class="text-[.78rem] font-semibold text-tcg12-muted">Cities</dt>
        </div>
      </dl>
    </div>

    <div class="grid [grid-template-columns:repeat(3,1fr)] [grid-template-rows:repeat(3,1fr)] gap-[clamp(8px,1vw,14px)] aspect-[1.05/1] min-h-[clamp(320px,42vw,560px)] max-[900px]:aspect-[1.4/1] max-[900px]:min-h-70 max-[560px]:aspect-square" aria-label="Featured stays">
      <div class="relative isolate col-start-1 col-span-2 row-start-1 row-span-2 flex items-end p-3 rounded-[14px] overflow-hidden bg-linear-[135deg,#1e3a5f,#0f1c2e] after:absolute after:inset-0 after:z-0 after:bg-linear-[180deg,transparent_45%,oklch(0_0_0/0.55)]">
        <span class="absolute top-2.5 right-2.5 z-2 px-2.25 py-1 rounded-full bg-white/92 backdrop-blur-[8px] text-tcg12-ink text-[.62rem] font-bold tracking-[0.1em] uppercase">Iceland</span>
        <span class="relative z-1 text-white text-[.86rem] font-bold [text-shadow:0_1px_3px_oklch(0_0_0/0.4)]">Black Sand Cabin</span>
      </div>
      <div class="relative isolate col-start-3 row-start-1 flex items-end p-3 rounded-[14px] overflow-hidden bg-linear-[135deg,#d97a99,#7a3855] after:absolute after:inset-0 after:z-0 after:bg-linear-[180deg,transparent_45%,oklch(0_0_0/0.55)]">
        <span class="absolute top-2.5 right-2.5 z-2 px-2.25 py-1 rounded-full bg-white/92 backdrop-blur-[8px] text-tcg12-ink text-[.62rem] font-bold tracking-[0.1em] uppercase">Tokyo</span>
        <span class="relative z-1 text-white text-[.86rem] font-bold [text-shadow:0_1px_3px_oklch(0_0_0/0.4)]">Shibuya Loft</span>
      </div>
      <div class="relative isolate col-start-3 row-start-2 flex items-end p-3 rounded-[14px] overflow-hidden bg-linear-[135deg,#f4a572,#d97f4f] after:absolute after:inset-0 after:z-0 after:bg-linear-[180deg,transparent_45%,oklch(0_0_0/0.55)]">
        <span class="absolute top-2.5 right-2.5 z-2 px-2.25 py-1 rounded-full bg-white/92 backdrop-blur-[8px] text-tcg12-ink text-[.62rem] font-bold tracking-[0.1em] uppercase">Lisbon</span>
        <span class="relative z-1 text-white text-[.86rem] font-bold [text-shadow:0_1px_3px_oklch(0_0_0/0.4)]">Alfama Studio</span>
      </div>
      <div class="relative isolate col-start-1 row-start-3 flex items-end p-3 rounded-[14px] overflow-hidden bg-linear-[135deg,#e6c54a,#a8732d] after:absolute after:inset-0 after:z-0 after:bg-linear-[180deg,transparent_45%,oklch(0_0_0/0.55)]">
        <span class="absolute top-2.5 right-2.5 z-2 px-2.25 py-1 rounded-full bg-white/92 backdrop-blur-[8px] text-tcg12-ink text-[.62rem] font-bold tracking-[0.1em] uppercase">Marrakech</span>
        <span class="relative z-1 text-white text-[.86rem] font-bold [text-shadow:0_1px_3px_oklch(0_0_0/0.4)]">Medina Riad</span>
      </div>
      <div class="relative isolate col-start-2 col-span-2 row-start-3 flex items-end p-3 rounded-[14px] overflow-hidden bg-linear-[135deg,#7aa852,#3d6a2a] after:absolute after:inset-0 after:z-0 after:bg-linear-[180deg,transparent_45%,oklch(0_0_0/0.55)]">
        <span class="absolute top-2.5 right-2.5 z-2 px-2.25 py-1 rounded-full bg-white/92 backdrop-blur-[8px] text-tcg12-ink text-[.62rem] font-bold tracking-[0.1em] uppercase">Brooklyn</span>
        <span class="relative z-1 text-white text-[.86rem] font-bold [text-shadow:0_1px_3px_oklch(0_0_0/0.4)]">Williamsburg Flat</span>
      </div>
    </div>

  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tcg12-bg: #fafaf7;
  --color-tcg12-bg: oklch(0.98 0.005 100);
  --color-tcg12-ink: #0f172a;
  --color-tcg12-ink: oklch(0.21 0.04 260);
  --color-tcg12-muted: #64748b;
  --color-tcg12-muted: oklch(0.55 0.03 257);
  --color-tcg12-line: #e2e8f0;
  --color-tcg12-line: oklch(0.92 0.01 255);
  --color-tcg12-accent: #dc2626;
  --color-tcg12-accent: oklch(0.58 0.22 27);
  --color-tcg12-accentsoft: #fbe3e3;
  --color-tcg12-accentsoft: oklch(0.93 0.04 27);
  --font-tcg12-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-tcg12-serif: 'Playfair Display', Georgia, serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh. */

.tcg-12 {
  width: 100%;
  min-height: 100svh;
}

.tcg-12 ::selection {
  background: #0f172a;
  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-tcg12-bg: #fafaf7;
  --color-tcg12-bg: oklch(0.98 0.005 100);
  --color-tcg12-ink: #0f172a;
  --color-tcg12-ink: oklch(0.21 0.04 260);
  --color-tcg12-muted: #64748b;
  --color-tcg12-muted: oklch(0.55 0.03 257);
  --color-tcg12-line: #e2e8f0;
  --color-tcg12-line: oklch(0.92 0.01 255);
  --color-tcg12-accent: #dc2626;
  --color-tcg12-accent: oklch(0.58 0.22 27);
  --color-tcg12-accentsoft: #fbe3e3;
  --color-tcg12-accentsoft: oklch(0.93 0.04 27);
  --font-tcg12-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-tcg12-serif: 'Playfair Display', Georgia, serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh. */

.tcg-12 {
  width: 100%;
  min-height: 100svh;
}

.tcg-12 ::selection {
  background: #0f172a;
  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&family=Playfair+Display:ital,wght@1,600;1,700&display=swap">

What changed in the port

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

  • Five .photo--N placement rules become col-start-*/row-start-*/*-span-* pairs on the tiles. This is the clearest win in the whole collection: a bento's rhythm belongs in the markup, because that is where you edit it.
  • The readability wash is an after: chain, so a tile is one element again instead of an element plus a stylesheet block. Tailwind's automatic content:&quot;&quot; is what makes that possible.
  • The metrics row is marked up as a <dl> rather than three divs — the port was the moment to fix the semantics, and the flex utilities are identical.
  • The CTA is a real <a> with min-h-11 and a focus-visible: ring; the CSS version styled hover only.
  • Both media queries survive as max-[900px]: and max-[560px]: variants, stated on the elements they reshape rather than in a block that has to re-select all of them.
  • The fonts 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.
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 Hero Photo Collage Grid
Source: https://codefronts.com/layouts/tailwind-card-grid-layout/tailwind-hero-photo-collage-grid/

A landing hero with narrative copy on the left and a five-tile asymmetric photo collage on the right. Every tile states its own placement &mdash; col-start-1 col-span-2 row-start-1 row-span-2 &mdash; so the collage rhythm is readable in the markup, and the readability overlay every photo grid ships is one after: chain instead of a pseudo-element block.
## HTML
```html
<section class="tcg-12 w-full min-h-svh flex items-center justify-center bg-tcg12-bg font-tcg12-sans text-tcg12-ink px-[clamp(20px,3vw,48px)] py-[clamp(28px,4vw,56px)]" aria-label="Hero photo collage grid demo">
  <div class="w-full max-w-[1200px] grid [grid-template-columns:1fr_1.2fr] max-[900px]:[grid-template-columns:1fr] items-center gap-[clamp(24px,3.4vw,56px)] max-[900px]:gap-8">

    <div class="flex flex-col gap-4.5 max-w-[520px] max-[900px]:max-w-none max-[900px]:items-center max-[900px]:text-center">
      <span class="w-fit px-3 py-1.5 rounded-full text-[.72rem] font-bold tracking-[0.16em] uppercase text-tcg12-accent bg-tcg12-accentsoft">New &middot; 2026 collection</span>
      <h2 class="text-[clamp(2.2rem,4.5vw,3.6rem)] font-black tracking-[-0.03em] leading-[1.02]">Stay places that<br><em class="font-tcg12-serif italic font-bold text-tcg12-accent">feel like home.</em></h2>
      <p class="max-w-[46ch] text-[clamp(.96rem,1.2vw,1.1rem)] leading-[1.55] text-tcg12-muted text-pretty">Hand-picked stays in 47 cities. Verified by humans. Booked by people who care where they sleep.</p>
      <div class="mt-1.5 flex flex-wrap items-center gap-4">
        <a href="#stays" class="inline-flex items-center min-h-11 px-5.5 py-3.5 rounded-[10px] bg-tcg12-ink text-white text-[.96rem] font-bold no-underline transition-[background,translate] duration-200 hover:bg-tcg12-accent hover:-translate-y-0.5 focus-visible:outline-2 focus-visible:outline-tcg12-accent focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:hover:translate-y-0">Browse stays &rarr;</a>
        <a href="#how" class="inline-flex items-center min-h-11 text-[.94rem] font-semibold text-tcg12-ink underline decoration-1 underline-offset-4 hover:text-tcg12-accent focus-visible:outline-2 focus-visible:outline-tcg12-accent focus-visible:outline-offset-3">How it works</a>
      </div>
      <dl class="mt-4 pt-6 flex gap-[clamp(20px,3vw,40px)] max-[560px]:gap-3.5 border-t border-tcg12-line">
        <div class="flex flex-col gap-0.5">
          <dd class="text-2xl max-[560px]:text-xl font-black tracking-[-0.02em] tabular-nums">4.9 &#9733;</dd>
          <dt class="text-[.78rem] font-semibold text-tcg12-muted">Avg rating</dt>
        </div>
        <div class="flex flex-col gap-0.5">
          <dd class="text-2xl max-[560px]:text-xl font-black tracking-[-0.02em] tabular-nums">12K+</dd>
          <dt class="text-[.78rem] font-semibold text-tcg12-muted">Verified stays</dt>
        </div>
        <div class="flex flex-col gap-0.5">
          <dd class="text-2xl max-[560px]:text-xl font-black tracking-[-0.02em] tabular-nums">47</dd>
          <dt class="text-[.78rem] font-semibold text-tcg12-muted">Cities</dt>
        </div>
      </dl>
    </div>

    <div class="grid [grid-template-columns:repeat(3,1fr)] [grid-template-rows:repeat(3,1fr)] gap-[clamp(8px,1vw,14px)] aspect-[1.05/1] min-h-[clamp(320px,42vw,560px)] max-[900px]:aspect-[1.4/1] max-[900px]:min-h-70 max-[560px]:aspect-square" aria-label="Featured stays">
      <div class="relative isolate col-start-1 col-span-2 row-start-1 row-span-2 flex items-end p-3 rounded-[14px] overflow-hidden bg-linear-[135deg,#1e3a5f,#0f1c2e] after:absolute after:inset-0 after:z-0 after:bg-linear-[180deg,transparent_45%,oklch(0_0_0/0.55)]">
        <span class="absolute top-2.5 right-2.5 z-2 px-2.25 py-1 rounded-full bg-white/92 backdrop-blur-[8px] text-tcg12-ink text-[.62rem] font-bold tracking-[0.1em] uppercase">Iceland</span>
        <span class="relative z-1 text-white text-[.86rem] font-bold [text-shadow:0_1px_3px_oklch(0_0_0/0.4)]">Black Sand Cabin</span>
      </div>
      <div class="relative isolate col-start-3 row-start-1 flex items-end p-3 rounded-[14px] overflow-hidden bg-linear-[135deg,#d97a99,#7a3855] after:absolute after:inset-0 after:z-0 after:bg-linear-[180deg,transparent_45%,oklch(0_0_0/0.55)]">
        <span class="absolute top-2.5 right-2.5 z-2 px-2.25 py-1 rounded-full bg-white/92 backdrop-blur-[8px] text-tcg12-ink text-[.62rem] font-bold tracking-[0.1em] uppercase">Tokyo</span>
        <span class="relative z-1 text-white text-[.86rem] font-bold [text-shadow:0_1px_3px_oklch(0_0_0/0.4)]">Shibuya Loft</span>
      </div>
      <div class="relative isolate col-start-3 row-start-2 flex items-end p-3 rounded-[14px] overflow-hidden bg-linear-[135deg,#f4a572,#d97f4f] after:absolute after:inset-0 after:z-0 after:bg-linear-[180deg,transparent_45%,oklch(0_0_0/0.55)]">
        <span class="absolute top-2.5 right-2.5 z-2 px-2.25 py-1 rounded-full bg-white/92 backdrop-blur-[8px] text-tcg12-ink text-[.62rem] font-bold tracking-[0.1em] uppercase">Lisbon</span>
        <span class="relative z-1 text-white text-[.86rem] font-bold [text-shadow:0_1px_3px_oklch(0_0_0/0.4)]">Alfama Studio</span>
      </div>
      <div class="relative isolate col-start-1 row-start-3 flex items-end p-3 rounded-[14px] overflow-hidden bg-linear-[135deg,#e6c54a,#a8732d] after:absolute after:inset-0 after:z-0 after:bg-linear-[180deg,transparent_45%,oklch(0_0_0/0.55)]">
        <span class="absolute top-2.5 right-2.5 z-2 px-2.25 py-1 rounded-full bg-white/92 backdrop-blur-[8px] text-tcg12-ink text-[.62rem] font-bold tracking-[0.1em] uppercase">Marrakech</span>
        <span class="relative z-1 text-white text-[.86rem] font-bold [text-shadow:0_1px_3px_oklch(0_0_0/0.4)]">Medina Riad</span>
      </div>
      <div class="relative isolate col-start-2 col-span-2 row-start-3 flex items-end p-3 rounded-[14px] overflow-hidden bg-linear-[135deg,#7aa852,#3d6a2a] after:absolute after:inset-0 after:z-0 after:bg-linear-[180deg,transparent_45%,oklch(0_0_0/0.55)]">
        <span class="absolute top-2.5 right-2.5 z-2 px-2.25 py-1 rounded-full bg-white/92 backdrop-blur-[8px] text-tcg12-ink text-[.62rem] font-bold tracking-[0.1em] uppercase">Brooklyn</span>
        <span class="relative z-1 text-white text-[.86rem] font-bold [text-shadow:0_1px_3px_oklch(0_0_0/0.4)]">Williamsburg Flat</span>
      </div>
    </div>

  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tcg12-bg: #fafaf7;
  --color-tcg12-bg: oklch(0.98 0.005 100);
  --color-tcg12-ink: #0f172a;
  --color-tcg12-ink: oklch(0.21 0.04 260);
  --color-tcg12-muted: #64748b;
  --color-tcg12-muted: oklch(0.55 0.03 257);
  --color-tcg12-line: #e2e8f0;
  --color-tcg12-line: oklch(0.92 0.01 255);
  --color-tcg12-accent: #dc2626;
  --color-tcg12-accent: oklch(0.58 0.22 27);
  --color-tcg12-accentsoft: #fbe3e3;
  --color-tcg12-accentsoft: oklch(0.93 0.04 27);
  --font-tcg12-sans: 'Space Grotesk', system-ui, sans-serif;
  --font-tcg12-serif: 'Playfair Display', Georgia, serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh. */

.tcg-12 {
  width: 100%;
  min-height: 100svh;
}

.tcg-12 ::selection {
  background: #0f172a;
  color: #fff;
}
```

How this works

The collage is nine cells and five tiles. The container is grid [grid-template-columns:repeat(3,1fr)] [grid-template-rows:repeat(3,1fr)] and each tile places itself:

<div class="col-start-1 col-span-2 row-start-1 row-span-2 …">

The CSS version wrote those five placements as five modifier classes in a stylesheet. As utilities they sit on the tiles, so rearranging the collage is a matter of editing the tile you are looking at — and the bento reads correctly in source order.

The overlay is a variant, not an element. White labels on unknown photo brightness need a wash; after: supplies content:"" automatically, so the whole treatment is four utilities. The label sits at z-1 and the pill at z-2, both above the wash at z-0.

Two breakpoints, both reshaping. At max-[900px]: the outer grid becomes one column and the copy centres; the collage keeps all five tiles but flattens to aspect-[1.4/1], and at max-[560px]: back to square. The point is that the collage never restacks into a photo list — on a phone it is still a collage.

Swapping in real photography is one utility per tile. bg-linear-[135deg,#1e3a5f,#0f1c2e] becomes bg-[url(hero-1.jpg)] bg-cover bg-center; the labels, pills and wash are unaffected because none of them depends on the background type.

Make it yours

  • Rebalance copy against collage by editing the outer [grid-template-columns:1fr_1.2fr] — 1fr_1.6fr is collage-led, 1.2fr_1fr is copy-led.
  • Swap any gradient for a photo: replace the tile's bg-linear-[…] with bg-[url(…)] bg-cover bg-center.
  • Add a sixth tile by widening the collage to [grid-template-columns:repeat(4,1fr)] and re-stating the spans on the tiles you want to grow.
  • Retheme every CTA and pill through --color-tcg12-accent in @theme.
  • Lighten the wash by moving the stop: after:bg-linear-[180deg,transparent_65%,oklch(0_0_0/0.45)].

Gotchas — read before shipping

  • Arbitrary values take no spaces: the track list must be typed [grid-template-columns:1fr_1.2fr], and every gradient stop pair uses the underscore escape.
  • col-start-* and row-start-* are 1-indexed grid lines, not cell indexes — a tile starting at column 3 is col-start-3, and col-span-2 from there overflows a 3-column grid.
  • Tiles spanning 2×2 of a 3×3 collage need source images of at least 800×600, or they read blocky next to the small tiles.
  • The pill needs z-2: with backdrop-blur but no stacking order it renders under the after: wash and looks muddy.
  • Keep isolation-isolate (or isolate) on the tile so the wash's z-index cannot escape into the page's stacking context.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Grid placement and gradients are baseline; backdrop-blur on the pills needs Safari 9+ with the -webkit- prefix, which Tailwind emits automatically. The floor quoted is Tailwind v4's own.

Techniques used in this demo

Arbitraryneeds bracket syntax

HTML & JSno Tailwind involved

Search CodeFronts

Loading…