Pure TailwindMIT licensed

Tailwind Equal-Height Product Grid

The alignment problem every catalogue has — titles of different lengths pushing the Add-to-Cart buttons out of line — solved in four utilities: items-stretch on the grid, h-full flex flex-col on the card, and mt-auto on the price row. The port's value here is that the contract is now visible on the elements that make it.

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-03 w-full min-h-svh bg-tcg03-bg font-tcg03-sans text-tcg03-ink px-6 py-12" aria-label="Equal-height product card grid demo">
  <div class="max-w-[980px] mx-auto grid [grid-template-columns:repeat(auto-fill,minmax(220px,1fr))] items-stretch gap-6">

    <article class="h-full flex flex-col p-5 rounded-[14px] border border-tcg03-line bg-tcg03-card transition-[translate,box-shadow] duration-250 hover:-translate-y-[3px] hover:shadow-[0_14px_30px_-18px_oklch(0.3_0.02_260/0.25)] focus-within:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0">
      <div class="w-full aspect-[4/3] mb-3.5 rounded-[10px] bg-linear-[135deg,#fde68a,#f59e0b]" aria-hidden="true"></div>
      <h3 class="text-[.98rem] font-bold leading-[1.35] mb-1.5">Ceramic Pour-Over Set</h3>
      <p class="text-[.82rem] leading-[1.5] text-tcg03-muted text-pretty">Hand-glazed dripper and carafe.</p>
      <div class="mt-auto pt-4 flex items-center justify-between gap-2.5">
        <span class="text-[1.05rem] font-extrabold tabular-nums">$48</span>
        <button type="button" class="min-h-11 px-3.5 rounded-lg border-0 bg-tcg03-ink text-white font-tcg03-sans text-[.78rem] font-bold cursor-pointer transition-[background] duration-200 hover:bg-tcg03-inkdeep focus-visible:outline-2 focus-visible:outline-tcg03-accent focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
      </div>
    </article>

    <article class="h-full flex flex-col p-5 rounded-[14px] border border-tcg03-line bg-tcg03-card transition-[translate,box-shadow] duration-250 hover:-translate-y-[3px] hover:shadow-[0_14px_30px_-18px_oklch(0.3_0.02_260/0.25)] focus-within:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0">
      <div class="w-full aspect-[4/3] mb-3.5 rounded-[10px] bg-linear-[135deg,#a7f3d0,#10b981]" aria-hidden="true"></div>
      <h3 class="text-[.98rem] font-bold leading-[1.35] mb-1.5">Walnut Cutting Board With Reversible Edge and Juice Groove</h3>
      <p class="text-[.82rem] leading-[1.5] text-tcg03-muted text-pretty">FSC-certified solid walnut, oiled and ready for daily use in any kitchen.</p>
      <div class="mt-auto pt-4 flex items-center justify-between gap-2.5">
        <span class="text-[1.05rem] font-extrabold tabular-nums">$72</span>
        <button type="button" class="min-h-11 px-3.5 rounded-lg border-0 bg-tcg03-ink text-white font-tcg03-sans text-[.78rem] font-bold cursor-pointer transition-[background] duration-200 hover:bg-tcg03-inkdeep focus-visible:outline-2 focus-visible:outline-tcg03-accent focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
      </div>
    </article>

    <article class="h-full flex flex-col p-5 rounded-[14px] border border-tcg03-line bg-tcg03-card transition-[translate,box-shadow] duration-250 hover:-translate-y-[3px] hover:shadow-[0_14px_30px_-18px_oklch(0.3_0.02_260/0.25)] focus-within:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0">
      <div class="w-full aspect-[4/3] mb-3.5 rounded-[10px] bg-linear-[135deg,#bfdbfe,#3b82f6]" aria-hidden="true"></div>
      <h3 class="text-[.98rem] font-bold leading-[1.35] mb-1.5">Linen Napkin Set</h3>
      <p class="text-[.82rem] leading-[1.5] text-tcg03-muted text-pretty">Set of four, stonewashed for softness.</p>
      <div class="mt-auto pt-4 flex items-center justify-between gap-2.5">
        <span class="text-[1.05rem] font-extrabold tabular-nums">$34</span>
        <button type="button" class="min-h-11 px-3.5 rounded-lg border-0 bg-tcg03-ink text-white font-tcg03-sans text-[.78rem] font-bold cursor-pointer transition-[background] duration-200 hover:bg-tcg03-inkdeep focus-visible:outline-2 focus-visible:outline-tcg03-accent focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
      </div>
    </article>

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

@theme {
  --color-tcg03-bg: #f8f7f4;
  --color-tcg03-bg: oklch(0.97 0.005 90);
  --color-tcg03-card: #ffffff;
  --color-tcg03-card: oklch(1 0 0);
  --color-tcg03-ink: #1f2937;
  --color-tcg03-ink: oklch(0.29 0.03 260);
  --color-tcg03-inkdeep: #111827;
  --color-tcg03-inkdeep: oklch(0.21 0.03 265);
  --color-tcg03-muted: #6b7280;
  --color-tcg03-muted: oklch(0.55 0.02 260);
  --color-tcg03-line: #e7e4dd;
  --color-tcg03-line: oklch(0.91 0.008 90);
  --color-tcg03-accent: #f59e0b;
  --color-tcg03-accent: oklch(0.77 0.16 70);
  --font-tcg03-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract. */

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

.tcg-03 ::selection {
  background: #f59e0b;
  color: #1a1a1a;
}

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-tcg03-bg: #f8f7f4;
  --color-tcg03-bg: oklch(0.97 0.005 90);
  --color-tcg03-card: #ffffff;
  --color-tcg03-card: oklch(1 0 0);
  --color-tcg03-ink: #1f2937;
  --color-tcg03-ink: oklch(0.29 0.03 260);
  --color-tcg03-inkdeep: #111827;
  --color-tcg03-inkdeep: oklch(0.21 0.03 265);
  --color-tcg03-muted: #6b7280;
  --color-tcg03-muted: oklch(0.55 0.02 260);
  --color-tcg03-line: #e7e4dd;
  --color-tcg03-line: oklch(0.91 0.008 90);
  --color-tcg03-accent: #f59e0b;
  --color-tcg03-accent: oklch(0.77 0.16 70);
  --font-tcg03-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract. */

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

.tcg-03 ::selection {
  background: #f59e0b;
  color: #1a1a1a;
}
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.

  • This is the smallest port in the collection and the clearest: four utilities on three elements now state the entire equal-height contract, where the CSS version stated it across four rules a designer had to assemble mentally.
  • auto-fill is preserved deliberately. It is one word different from demo 01's auto-fit and it is the difference between keeping empty tracks and collapsing them.
  • The card gained focus-within:-translate-y-[3px] so tabbing to the Add-to-Cart button lifts the card the same way hovering does — a keyboard user now gets the same feedback.
  • The button went from 34px tall to min-h-11. The visual change is small; the compliance change is the point.
  • line-clamp-2 is offered as a customization rather than shipped, because clamping is a content decision and the auto-margin already solves the alignment.
  • The reduced-motion block becomes motion-reduce:transition-none motion-reduce:hover:translate-y-0 on the card and the button, which is two utilities each instead of a selector list at the bottom of the sheet.
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 Equal-Height Product Grid
Source: https://codefronts.com/layouts/tailwind-card-grid-layout/tailwind-equal-height-product-grid/

The alignment problem every catalogue has &mdash; titles of different lengths pushing the Add-to-Cart buttons out of line &mdash; solved in four utilities: items-stretch on the grid, h-full flex flex-col on the card, and mt-auto on the price row. The port's value here is that the contract is now visible on the elements that make it.
## HTML
```html
<section class="tcg-03 w-full min-h-svh bg-tcg03-bg font-tcg03-sans text-tcg03-ink px-6 py-12" aria-label="Equal-height product card grid demo">
  <div class="max-w-[980px] mx-auto grid [grid-template-columns:repeat(auto-fill,minmax(220px,1fr))] items-stretch gap-6">

    <article class="h-full flex flex-col p-5 rounded-[14px] border border-tcg03-line bg-tcg03-card transition-[translate,box-shadow] duration-250 hover:-translate-y-[3px] hover:shadow-[0_14px_30px_-18px_oklch(0.3_0.02_260/0.25)] focus-within:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0">
      <div class="w-full aspect-[4/3] mb-3.5 rounded-[10px] bg-linear-[135deg,#fde68a,#f59e0b]" aria-hidden="true"></div>
      <h3 class="text-[.98rem] font-bold leading-[1.35] mb-1.5">Ceramic Pour-Over Set</h3>
      <p class="text-[.82rem] leading-[1.5] text-tcg03-muted text-pretty">Hand-glazed dripper and carafe.</p>
      <div class="mt-auto pt-4 flex items-center justify-between gap-2.5">
        <span class="text-[1.05rem] font-extrabold tabular-nums">$48</span>
        <button type="button" class="min-h-11 px-3.5 rounded-lg border-0 bg-tcg03-ink text-white font-tcg03-sans text-[.78rem] font-bold cursor-pointer transition-[background] duration-200 hover:bg-tcg03-inkdeep focus-visible:outline-2 focus-visible:outline-tcg03-accent focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
      </div>
    </article>

    <article class="h-full flex flex-col p-5 rounded-[14px] border border-tcg03-line bg-tcg03-card transition-[translate,box-shadow] duration-250 hover:-translate-y-[3px] hover:shadow-[0_14px_30px_-18px_oklch(0.3_0.02_260/0.25)] focus-within:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0">
      <div class="w-full aspect-[4/3] mb-3.5 rounded-[10px] bg-linear-[135deg,#a7f3d0,#10b981]" aria-hidden="true"></div>
      <h3 class="text-[.98rem] font-bold leading-[1.35] mb-1.5">Walnut Cutting Board With Reversible Edge and Juice Groove</h3>
      <p class="text-[.82rem] leading-[1.5] text-tcg03-muted text-pretty">FSC-certified solid walnut, oiled and ready for daily use in any kitchen.</p>
      <div class="mt-auto pt-4 flex items-center justify-between gap-2.5">
        <span class="text-[1.05rem] font-extrabold tabular-nums">$72</span>
        <button type="button" class="min-h-11 px-3.5 rounded-lg border-0 bg-tcg03-ink text-white font-tcg03-sans text-[.78rem] font-bold cursor-pointer transition-[background] duration-200 hover:bg-tcg03-inkdeep focus-visible:outline-2 focus-visible:outline-tcg03-accent focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
      </div>
    </article>

    <article class="h-full flex flex-col p-5 rounded-[14px] border border-tcg03-line bg-tcg03-card transition-[translate,box-shadow] duration-250 hover:-translate-y-[3px] hover:shadow-[0_14px_30px_-18px_oklch(0.3_0.02_260/0.25)] focus-within:-translate-y-[3px] motion-reduce:transition-none motion-reduce:hover:translate-y-0">
      <div class="w-full aspect-[4/3] mb-3.5 rounded-[10px] bg-linear-[135deg,#bfdbfe,#3b82f6]" aria-hidden="true"></div>
      <h3 class="text-[.98rem] font-bold leading-[1.35] mb-1.5">Linen Napkin Set</h3>
      <p class="text-[.82rem] leading-[1.5] text-tcg03-muted text-pretty">Set of four, stonewashed for softness.</p>
      <div class="mt-auto pt-4 flex items-center justify-between gap-2.5">
        <span class="text-[1.05rem] font-extrabold tabular-nums">$34</span>
        <button type="button" class="min-h-11 px-3.5 rounded-lg border-0 bg-tcg03-ink text-white font-tcg03-sans text-[.78rem] font-bold cursor-pointer transition-[background] duration-200 hover:bg-tcg03-inkdeep focus-visible:outline-2 focus-visible:outline-tcg03-accent focus-visible:outline-offset-2 motion-reduce:transition-none">Add to Cart</button>
      </div>
    </article>

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

@theme {
  --color-tcg03-bg: #f8f7f4;
  --color-tcg03-bg: oklch(0.97 0.005 90);
  --color-tcg03-card: #ffffff;
  --color-tcg03-card: oklch(1 0 0);
  --color-tcg03-ink: #1f2937;
  --color-tcg03-ink: oklch(0.29 0.03 260);
  --color-tcg03-inkdeep: #111827;
  --color-tcg03-inkdeep: oklch(0.21 0.03 265);
  --color-tcg03-muted: #6b7280;
  --color-tcg03-muted: oklch(0.55 0.02 260);
  --color-tcg03-line: #e7e4dd;
  --color-tcg03-line: oklch(0.91 0.008 90);
  --color-tcg03-accent: #f59e0b;
  --color-tcg03-accent: oklch(0.77 0.16 70);
  --font-tcg03-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract. */

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

.tcg-03 ::selection {
  background: #f59e0b;
  color: #1a1a1a;
}
```

How this works

Grid stretches; flex distributes. The track list gives equal columns and grid's default align-items: stretch makes every cell as tall as the tallest in its row. The card then has to actually fill that cell:

<article class="h-full flex flex-col …">
  …
  <div class="mt-auto pt-4 flex items-center justify-between">

h-full is the utility people forget; without it the card hugs its content and the whole trick quietly fails. mt-auto then pushes the price row to the bottom regardless of whether the title wrapped once or three times.

The image is part of the alignment. aspect-[4/3] guarantees every card's media block is the same height, so the text starts on a shared line too — bottom alignment alone still looks ragged if the images differ.

auto-fill, not auto-fit. The original chose auto-fill so a short catalogue keeps its column rhythm instead of stretching three cards across a 1400px row; the port keeps that choice, and it is the one-word difference worth knowing between this demo and demo 01.

Make it yours

  • Add line-clamp-2 to the title for strict two-line parity before the auto-margin does its work.
  • Swap the CTA for an icon button: replace the button's padding with w-10 h-10 rounded-full; mt-auto on the row is unaffected.
  • Densify or loosen the whole catalogue by editing the 220px floor in the arbitrary track list.
  • Add a sale price by wrapping the old figure in <s class=&quot;opacity-50&quot;> inside the price cell.
  • Retheme through --color-tcg03-bg, --color-tcg03-ink and --color-tcg03-accent in @theme.

Gotchas — read before shipping

  • Without h-full on the card the alignment silently breaks: grid stretches the cell, the card does not fill it, and mt-auto has no leftover space to absorb.
  • An intrinsic <img> with no aspect-* reintroduces misalignment above the text even though the CTA row still lines up.
  • items-stretch is grid's default, but an inherited items-start from a parent layout will override it — state it explicitly, as this demo does.
  • auto-fill keeps empty tracks; on an ultra-wide screen with three products you get three cards and empty columns. That is the intended behaviour here — use auto-fit if you want them to stretch.
  • The Add-to-Cart button needs min-h-11: a 34px-tall button is under the 44px touch-target minimum, which is what the CSS version shipped.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Grid auto-stretch plus flexbox auto-margins are baseline everywhere; aspect-ratio needs Safari 15+. The floor quoted is Tailwind v4's own generated sheet.

Techniques used in this demo

HTML & JSno Tailwind involved

Search CodeFronts

Loading…