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.
- 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>Sample Run
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.