LearnAI ToolsCareerPractice BuildsPlayContact
Tailwind CSSBeginner~1 hour

Responsive Card Grid

Build a responsive grid of product cards using flexbox/grid utilities.

Grid UtilitiesResponsive DesignCards

Overview

A grid of product cards is one of the most common layouts on the web — e-commerce listings, blog indexes, and dashboards all reduce to the same shape: a repeating card that reflows from one column on a phone to several columns on a desktop. This project builds exactly that using nothing but Tailwind utility classes, with zero custom CSS written anywhere.

The core lesson here is Tailwind's mobile-first responsive system: an unprefixed class like `grid-cols-1` applies at every screen size by default, and prefixes like `sm:` and `lg:` only override it once the viewport reaches that breakpoint. Stacking a few of these on one element is enough to produce a fully responsive grid without a single `@media` query in sight.

What You'll Build
  • A product grid that goes from 1 column on mobile, to 2 on tablets, to 3 (or 4) on desktop.
  • Individual product cards with an image, name, price, description, and a call-to-action button.
  • Smooth hover states — the card lifts and its image zooms slightly on hover.
  • Even, responsive spacing between cards using the `gap-*` utilities instead of manual margins.

Prerequisites

  • Utility-first fundamentals — applying classes like `p-4`, `rounded-lg`, or `bg-white` directly in markup.
  • The Tailwind spacing scale — how `gap-4`, `p-5`, `mb-2` etc. map to consistent rem-based values.
  • Responsive prefixes — `sm:`, `md:`, `lg:`, `xl:` and Tailwind's mobile-first breakpoint model.
  • State variants — `hover:` classes and how they only apply on top of an element's base classes.
  • Basic Flexbox/Grid vocabulary — knowing what a "column" and a "gutter" refer to in a grid layout.

Project Structure

This project is a single HTML file — Tailwind's utility classes are written directly on elements, so there is no separate stylesheet to maintain. The only setup required is loading the Tailwind Play CDN script in `<head>`, which scans the page's markup and generates whatever utility CSS it finds referenced, live in the browser with zero build step:

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Compiles Tailwind utility classes on the fly, in the browser — no npm install or build step needed -->
<script src="https://cdn.tailwindcss.com"></script>
</head>

With that in place, every step below is purely about which utility classes go on which elements.

Step 1: Set Up the Page and Grid Container

Before any cards exist, the page needs a background, a centered content column, and — the star of this project — the grid container itself. Getting the container's responsive column classes right up front means every card added later just slots into place automatically.

<!-- Page wrapper: light gray background, generous vertical padding -->
<div class="min-h-screen bg-slate-100 py-12 px-6">
<!-- max-w-6xl caps the content width on very wide monitors; mx-auto centers it horizontally -->
<div class="max-w-6xl mx-auto">
<h1 class="text-3xl font-bold text-slate-900 mb-8 text-center">Featured Products</h1>
<!-- The grid container: -->
<!-- grid-cols-1 -> unprefixed = applies at EVERY width by default (mobile-first baseline) -->
<!-- sm:grid-cols-2 -> overrides to 2 columns from 640px up, once a second column comfortably fits -->
<!-- lg:grid-cols-3 -> overrides again to 3 columns from 1024px up, a typical laptop/desktop width -->
<!-- gap-6 -> a consistent 1.5rem gutter between every card, both horizontally and vertically -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Step 2 builds the card that repeats inside here -->
</div>
</div>
</div>

Step 2: Build a Single Product Card

Each card is a small, self-contained block: an image on top, then a padded content area with the product name and price on one row, a short description, and a full-width button. `overflow-hidden` on the card is what makes the image's square corners get clipped to match the card's own rounded corners.

<!-- bg-white + shadow-md lifts the card off the gray page background from Step 1 -->
<!-- overflow-hidden clips the <img> below to the card's rounded-xl corner radius -->
<article class="bg-white rounded-xl shadow-md overflow-hidden">
<img src="https://picsum.photos/seed/headphones/400/300" alt="Wireless headphones" class="w-full h-44 object-cover">
<!-- object-cover crops the image to fill w-full/h-44 exactly, instead of squashing its aspect ratio -->
<div class="p-5">
<!-- flex + justify-between: name sits left, price is pinned to the far right, on one row -->
<div class="flex items-start justify-between gap-2 mb-1">
<h3 class="font-semibold text-slate-900">Wireless Headphones</h3>
<span class="text-indigo-600 font-bold whitespace-nowrap">$89.99</span>
<!-- whitespace-nowrap stops the price from wrapping onto two lines if the name is long -->
</div>
<p class="text-sm text-slate-500 mb-4">Noise-cancelling, 30-hour battery, USB-C fast charging.</p>
<button class="w-full bg-indigo-600 text-white text-sm font-semibold py-2 rounded-lg">
Add to Cart
</button>
</div>
</article>

Step 3: Add Hover States and Polish

A static card grid works, but `hover:` variants are what make it feel alive. Tailwind applies these classes conditionally — the browser only computes them once the pointer is actually over the element — so no JavaScript or `:hover` CSS rule is written by hand anywhere.

<!-- Same card as Step 2, with transition + hover: utilities layered on -->
<article class="bg-white rounded-xl shadow-md overflow-hidden
transition-all duration-300
hover:shadow-xl hover:-translate-y-1">
<!-- transition-all + duration-300: makes the shadow/transform changes below ANIMATE over 300ms
instead of snapping instantly — duration-300 only has an effect because transition-all exists -->
<!-- hover:shadow-xl swaps the resting shadow-md for a bigger, more dramatic shadow on hover -->
<!-- hover:-translate-y-1 nudges the whole card up 0.25rem, reinforcing the "lift" the shadow implies -->
<div class="overflow-hidden">
<!-- This wrapper's own overflow-hidden clips the zoomed image below so it doesn't spill past the card -->
<img src="https://picsum.photos/seed/headphones/400/300" alt="Wireless headphones"
class="w-full h-44 object-cover transition-transform duration-300 hover:scale-110">
<!-- hover:scale-110 zooms the image 10% on hover, a common "product photo" interaction cue -->
</div>
<div class="p-5">
<div class="flex items-start justify-between gap-2 mb-1">
<h3 class="font-semibold text-slate-900">Wireless Headphones</h3>
<span class="text-indigo-600 font-bold whitespace-nowrap">$89.99</span>
</div>
<p class="text-sm text-slate-500 mb-4">Noise-cancelling, 30-hour battery, USB-C fast charging.</p>
<button class="w-full bg-indigo-600 text-white text-sm font-semibold py-2 rounded-lg
transition-colors hover:bg-indigo-700">
<!-- transition-colors (rather than transition-all) here is a small optimization: this button
only ever animates its background color, so there's no reason to watch every property -->
Add to Cart
</button>
</div>
</article>

Step 4: Make the Grid Responsive Across Breakpoints

The grid from Step 1 already reflows at `sm` and `lg`, but a wide desktop monitor has room for a fourth column, and a phone benefits from a slightly tighter gutter than a desktop needs. Both are one extra responsive class each — no restructuring required.

<!-- xl:grid-cols-4 adds a fourth column once the viewport reaches 1280px, for very wide monitors -->
<!-- gap-4 sm:gap-6: a tighter 1rem gutter on phones (less horizontal room to spare), opening up
to the full 1.5rem gutter from "sm" up -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6">
<!-- ...cards from Steps 2-3 repeat here... -->
</div>

Complete Code

Here is the full page with the responsive grid container and six product cards assembled from every step above.

<div class="min-h-screen bg-slate-100 py-12 px-6">
<div class="max-w-6xl mx-auto">
<h1 class="text-3xl font-bold text-slate-900 mb-8 text-center">Featured Products</h1>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6">
<article class="bg-white rounded-xl shadow-md overflow-hidden transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
<div class="overflow-hidden">
<img src="https://picsum.photos/seed/headphones/400/300" alt="Wireless headphones" class="w-full h-44 object-cover transition-transform duration-300 hover:scale-110">
</div>
<div class="p-5">
<div class="flex items-start justify-between gap-2 mb-1">
<h3 class="font-semibold text-slate-900">Wireless Headphones</h3>
<span class="text-indigo-600 font-bold whitespace-nowrap">$89.99</span>
</div>
<p class="text-sm text-slate-500 mb-4">Noise-cancelling, 30-hour battery, USB-C fast charging.</p>
<button class="w-full bg-indigo-600 text-white text-sm font-semibold py-2 rounded-lg transition-colors hover:bg-indigo-700">Add to Cart</button>
</div>
</article>
<article class="bg-white rounded-xl shadow-md overflow-hidden transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
<div class="overflow-hidden">
<img src="https://picsum.photos/seed/watch/400/300" alt="Smart watch" class="w-full h-44 object-cover transition-transform duration-300 hover:scale-110">
</div>
<div class="p-5">
<div class="flex items-start justify-between gap-2 mb-1">
<h3 class="font-semibold text-slate-900">Smart Watch</h3>
<span class="text-indigo-600 font-bold whitespace-nowrap">$149.00</span>
</div>
<p class="text-sm text-slate-500 mb-4">Heart-rate tracking, 7-day battery, always-on display.</p>
<button class="w-full bg-indigo-600 text-white text-sm font-semibold py-2 rounded-lg transition-colors hover:bg-indigo-700">Add to Cart</button>
</div>
</article>
<article class="bg-white rounded-xl shadow-md overflow-hidden transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
<div class="overflow-hidden">
<img src="https://picsum.photos/seed/speaker/400/300" alt="Bluetooth speaker" class="w-full h-44 object-cover transition-transform duration-300 hover:scale-110">
</div>
<div class="p-5">
<div class="flex items-start justify-between gap-2 mb-1">
<h3 class="font-semibold text-slate-900">Bluetooth Speaker</h3>
<span class="text-indigo-600 font-bold whitespace-nowrap">$59.50</span>
</div>
<p class="text-sm text-slate-500 mb-4">Waterproof, 12-hour playback, pair two for stereo.</p>
<button class="w-full bg-indigo-600 text-white text-sm font-semibold py-2 rounded-lg transition-colors hover:bg-indigo-700">Add to Cart</button>
</div>
</article>
<article class="bg-white rounded-xl shadow-md overflow-hidden transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
<div class="overflow-hidden">
<img src="https://picsum.photos/seed/camera/400/300" alt="Mirrorless camera" class="w-full h-44 object-cover transition-transform duration-300 hover:scale-110">
</div>
<div class="p-5">
<div class="flex items-start justify-between gap-2 mb-1">
<h3 class="font-semibold text-slate-900">Mirrorless Camera</h3>
<span class="text-indigo-600 font-bold whitespace-nowrap">$599.00</span>
</div>
<p class="text-sm text-slate-500 mb-4">4K video, interchangeable lenses, in-body stabilization.</p>
<button class="w-full bg-indigo-600 text-white text-sm font-semibold py-2 rounded-lg transition-colors hover:bg-indigo-700">Add to Cart</button>
</div>
</article>
<article class="bg-white rounded-xl shadow-md overflow-hidden transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
<div class="overflow-hidden">
<img src="https://picsum.photos/seed/keyboard/400/300" alt="Mechanical keyboard" class="w-full h-44 object-cover transition-transform duration-300 hover:scale-110">
</div>
<div class="p-5">
<div class="flex items-start justify-between gap-2 mb-1">
<h3 class="font-semibold text-slate-900">Mechanical Keyboard</h3>
<span class="text-indigo-600 font-bold whitespace-nowrap">$119.00</span>
</div>
<p class="text-sm text-slate-500 mb-4">Hot-swappable switches, per-key RGB, USB-C detachable cable.</p>
<button class="w-full bg-indigo-600 text-white text-sm font-semibold py-2 rounded-lg transition-colors hover:bg-indigo-700">Add to Cart</button>
</div>
</article>
<article class="bg-white rounded-xl shadow-md overflow-hidden transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
<div class="overflow-hidden">
<img src="https://picsum.photos/seed/mouse/400/300" alt="Wireless mouse" class="w-full h-44 object-cover transition-transform duration-300 hover:scale-110">
</div>
<div class="p-5">
<div class="flex items-start justify-between gap-2 mb-1">
<h3 class="font-semibold text-slate-900">Wireless Mouse</h3>
<span class="text-indigo-600 font-bold whitespace-nowrap">$39.99</span>
</div>
<p class="text-sm text-slate-500 mb-4">Silent clicks, 4000 DPI sensor, 3-month battery life.</p>
<button class="w-full bg-indigo-600 text-white text-sm font-semibold py-2 rounded-lg transition-colors hover:bg-indigo-700">Add to Cart</button>
</div>
</article>
</div>
</div>
</div>
Live Preview

Sample Run

What You'll See

Click Run to see what this code prints.

Extend This Project

  • Add category filter buttons above the grid that toggle `hidden` on non-matching cards with a few lines of JavaScript.
  • Add a "Sale" badge using `absolute` positioning on a card with `relative`, similar to the featured-plan badge trick from a pricing page.
  • Build a skeleton loading state for each card using `animate-pulse` and gray placeholder blocks while "data" loads.
  • Swap the static product data for a real array and `.map()` over it in React/Next.js, generating the same cards dynamically.
  • Add a `dark:` themed version of the grid, reusing the dark-mode techniques from this course's Dark Mode Landing Page project.

Summary

You built a fully responsive product card grid using only Tailwind utility classes — no custom CSS file, no media queries written by hand. The `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3` pattern from this project is one of the most reused layout idioms in Tailwind, and the `hover:`/`transition-*` combination you applied to the cards is the same pattern you'll reach for anywhere an element needs a smooth, interactive hover state.