Summit Themes
Blog

Creating a complex responsive grid layout with Tailwind CSS

CSS Grid is one of the most powerful layout tools on the web, and Tailwind CSS makes it approachable without giving anything up. With v4's CSS-first configuration, you also get clean custom column definitions that slot right into your design system. This tutorial walks through building a genuinely complex responsive grid — the kind you'd find on a real services site — covering span control, asymmetric columns, subgrid, and custom named templates.

The techniques here scale from a simple three-column card row up to a full magazine-style layout. Every code block is copy-pasteable and uses the Tailwind v4 API.

The basics: turning a div into a grid

Adding grid to an element switches its children to grid placement. Pair it with grid-cols-<number> to set equal-width columns:

<div class="grid grid-cols-3 gap-6">
  <div>Card one</div>
  <div>Card two</div>
  <div>Card three</div>
</div>

grid-cols-3 compiles to grid-template-columns: repeat(3, minmax(0, 1fr)). The minmax(0, 1fr) part is important — it prevents content from blowing out column widths, which a plain 1fr can do when children contain long text or images.

The gap-6 utility sets both row and column gaps. Use gap-x-6 and gap-y-4 separately when you want different horizontal and vertical spacing.

Making it responsive

Tailwind's breakpoints are mobile-first: an unprefixed utility applies everywhere, and a prefixed one (md:, lg:, etc.) applies at that width and up. The five defaults in v4 are sm (640px), md (768px), lg (1024px), xl (1280px), and 2xl (1536px).

A typical responsive card grid stacks on mobile, shows two columns on tablet, and three on desktop:

<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
  <div class="rounded-xl bg-white p-6 shadow">HVAC Tune-Up</div>
  <div class="rounded-xl bg-white p-6 shadow">Duct Cleaning</div>
  <div class="rounded-xl bg-white p-6 shadow">Emergency Repair</div>
  <div class="rounded-xl bg-white p-6 shadow">System Install</div>
  <div class="rounded-xl bg-white p-6 shadow">Annual Inspection</div>
  <div class="rounded-xl bg-white p-6 shadow">Filter Replacement</div>
</div>

Nothing complicated, but this pattern handles the vast majority of card grids you'll build.

Spanning columns: breaking the uniform grid

A grid becomes visually interesting when certain items span multiple columns. Use col-span-<number> on a child to stretch it across columns, and col-span-full to stretch it all the way across.

<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
  <!-- Featured card spans all three columns on desktop -->
  <div class="md:col-span-3 rounded-xl bg-blue-600 text-white p-8">
    <h2>New: Same-Day Service Available</h2>
    <p>Book before noon, we arrive today.</p>
  </div>

  <!-- Regular cards fill the remaining columns -->
  <div class="rounded-xl bg-white p-6 shadow">Tune-Up</div>
  <div class="rounded-xl bg-white p-6 shadow">Inspection</div>
  <div class="rounded-xl bg-white p-6 shadow">Emergency Repair</div>
</div>

You can combine span with explicit placement using col-start-<number> and col-end-<number> when you need precise control over where an item sits, rather than relying on auto-placement:

<div class="col-start-2 col-span-2">Starts at column line 2, spans two</div>

Asymmetric layouts: mixing fixed and flexible columns

Equal-width columns work for card grids, but sidebar layouts need something different — one narrow fixed column and one wide flexible one, or a 2/3 + 1/3 split. In Tailwind v4 you have two clean ways to express this.

Arbitrary value syntax

For a one-off layout, use the bracket syntax to pass any valid CSS value directly:

<div class="grid grid-cols-[280px_1fr] gap-8">
  <aside class="bg-slate-50 rounded-xl p-4">Sidebar</aside>
  <main>Main content</main>
</div>

Note the underscore — Tailwind uses underscores in arbitrary values to represent spaces.

Custom named columns via @theme

If the same column structure appears in multiple places, register it as a named token in your CSS. Tailwind v4's CSS-first configuration uses the @theme block in your main stylesheet:

@import "tailwindcss";

@theme {
  --grid-template-columns-sidebar: 280px 1fr;
  --grid-template-columns-content: 1fr 2fr 1fr;
  --grid-template-columns-magazine: 1fr 1fr 340px;
}

Those three variables automatically generate utility classes: grid-cols-sidebar, grid-cols-content, and grid-cols-magazine. Use them exactly like built-in utilities — including with breakpoint prefixes:

<div class="grid grid-cols-1 lg:grid-cols-magazine gap-6">
  <article>Main story</article>
  <section>Secondary stories</section>
  <aside>Newsletter / ads</aside>
</div>

This is the right approach for design systems. The tokens live in one place, and every template that uses them stays in sync automatically.

A real-world complex layout

Let's put it all together. Here's a services-page hero section: a full-width headline, then a 2-wide feature card next to two stacked smaller cards, then a three-column stats row.

<section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-8">

  <!-- Full-width headline -->
  <div class="md:col-span-2 lg:col-span-3 text-center py-12">
    <h2 class="text-4xl font-bold">Why local contractors choose us</h2>
  </div>

  <!-- Large feature card: 2 cols wide on lg -->
  <div class="lg:col-span-2 bg-blue-600 text-white rounded-2xl p-8 flex flex-col justify-between min-h-64">
    <span class="text-sm font-semibold uppercase tracking-widest opacity-75">Featured</span>
    <div>
      <h3 class="text-2xl font-bold mb-2">Lifetime updates included</h3>
      <p class="opacity-90">Every theme ships with free updates as Astro and Tailwind evolve. No subscription.</p>
    </div>
  </div>

  <!-- Two stacked smaller cards in the third column -->
  <div class="flex flex-col gap-6">
    <div class="bg-slate-100 rounded-2xl p-6 flex-1">
      <h3 class="font-bold mb-1">100 Lighthouse score</h3>
      <p class="text-sm text-slate-600">Performance and SEO baked in from day one.</p>
    </div>
    <div class="bg-slate-100 rounded-2xl p-6 flex-1">
      <h3 class="font-bold mb-1">AI-ready via llms.txt</h3>
      <p class="text-sm text-slate-600">AI editors understand your theme's structure out of the box.</p>
    </div>
  </div>

  <!-- Three-column stats row -->
  <div class="md:col-span-2 lg:col-span-3 grid grid-cols-1 sm:grid-cols-3 gap-4">
    <div class="rounded-xl bg-white border border-slate-200 p-6 text-center">
      <p class="text-3xl font-bold">5</p>
      <p class="text-slate-500 text-sm mt-1">Themes shipping now</p>
    </div>
    <div class="rounded-xl bg-white border border-slate-200 p-6 text-center">
      <p class="text-3xl font-bold">~4 hrs</p>
      <p class="text-slate-500 text-sm mt-1">Average setup time</p>
    </div>
    <div class="rounded-xl bg-white border border-slate-200 p-6 text-center">
      <p class="text-3xl font-bold">$0</p>
      <p class="text-slate-500 text-sm mt-1">Ongoing platform fees</p>
    </div>
  </div>

</section>

This pattern — outer grid for the macro layout, inner grid for the stats row — is called a nested grid. Each grid is independent; items in the inner grid only respond to the inner grid's tracks.

Subgrid: when nested items need to align to the parent's tracks

Nested grids don't share track definitions with their parent, so children across siblings rarely line up vertically. Subgrid fixes this. A child set to grid-cols-subgrid inherits the parent's column tracks instead of defining new ones:

<div class="grid grid-cols-4 gap-4">
  <!-- This child spans all 4 tracks and uses the parent's tracks for its own children -->
  <div class="col-span-4 grid grid-cols-subgrid gap-4">
    <div class="col-start-1">Aligned to track 1</div>
    <div class="col-start-3 col-span-2">Aligned to tracks 3–4</div>
  </div>
</div>

This is most useful when you have a card grid where each card contains a header, body, and footer, and you want the footers of every card to sit on the same row regardless of content height. Subgrid is supported in all modern browsers and Tailwind v4 includes it as a first-class utility via grid-cols-subgrid and grid-rows-subgrid.

Common pitfalls and how to avoid them

Items stretching unexpectedly

Grid items stretch to fill their cell by default. If a card looks taller than expected, check whether it's being stretched by the grid's align-items: stretch default. Add items-start to the grid container or self-start to the specific item to stop this.

The last row being only half full

When a grid doesn't divide evenly, the last row has empty cells. Usually this is fine. But if you want the items in the last partial row to expand to fill the available space, you can switch to an auto-fill approach using an arbitrary template: grid-cols-[repeat(auto-fill,minmax(280px,1fr))]. This creates as many columns as fit at minimum 280px wide, and items flow naturally without you specifying a column count at all.

Forgetting that gap isn't margin

Grid gap only creates space between tracks, not around the outside edge of the grid. Add padding to the grid container itself (p-6) for outer spacing, or wrap it in a section with padding.

Wrapping up

A complex Tailwind grid layout isn't one big trick — it's a handful of composable tools used together: grid-cols-* for track definitions, breakpoint prefixes for responsive changes, col-span-* for visual hierarchy, arbitrary values or @theme tokens for asymmetric columns, and subgrid when children need to share parent tracks. Start with the simplest version that works, then layer in complexity only where the design actually needs it. The result is a layout system that's both highly readable and genuinely capable.