No Time Dad

A blog about web development written by a busy dad

Tailwind Responsive Grid with Cards

More fun with Tailwind today, as I build a simple card element and a responsive grid. I wanted to go through a few of Tailwind’s different breakpoint values to see how it would look with different grid columns counts.

It was surprisingly easy to create the responsive grid with Tailwind, but I did end up thinking I needed another breakpoint between Tailwind’s sm and md breakpoints. I didn’t end up implementing it, but I can add as many custom breakpoints to tailwind.config.js as I want.

Even before I started really using Tailwind in projects, I’ve been enjoying “mobile first” design. I always over thought responsive design in the past. It somehow seemed like some mythical kind of web development that was beyond my abilities. I guess many things in web development or software development in general are like this. Which is probably why I, and many others, get hit with imposter syndrome.

The truth is that responsive design can be as complicated or as simple as you want it to be. Simple is so much better, though. I might want that super fancy responsive site with all the bells and whistles, but it’s not always best long term. Eventually, those bells and whistles are going to need some work and it’s going to be a headache.

I also feel that most users don’t care that much about bells and whistles. A page that just works and loads quickly is more important, in my opinion.

Building a card

Before I can build a responsive grid using Tailwind, I need something to put in it. Probably one of the more common things I put in grids is cards. So, I’ll create a simple re-usable card component with React that I can use to fill my grid. The shape of the card below is defined by Tailwind’s shadow utility class for box-shadow.

I tried to keep the card as simple as possible, but my curiosity got the better of me and I thought the card might look better with an image header. So, I grabbed some random images from unplash to use as headers for my cards. The image has round top corners and straight bottom corners. I’m not sure where this style came from, but I like it.

There is also some basic spacing and font size changes via Tailwind’s utility classes, but overall the card is simple and doesn’t do much. I took the time to add an image but didn’t bother to make the card interactive or clickable. This is how my mind works.

const Card = ({ title, subtitle, imgSrc, imgAlt, bodyText }) => (
  <div className="shadow rounded-lg">
    <img className="rounded-tl-lg rounded-tr-lg h-52 w-full" src={imgSrc} alt={imgAlt} />
    <div className="p-2">
      <div className="text-2xl font-semibold">{title}</div>
      <div className="opacity-70">{subtitle}</div>
    </div>
    <div className="px-2 pb-4">
      {bodyText}
    </div>
  </div>
);
airtag
Card 1
Subtitle
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Building the grid

Now that I have something to put in my grid, I can create the basic grid structure. On small screens the grid should be a single column, and the column count should increase as the screen size grows.

The single column grid is such a strange sticking point for me. The page itself is a single column before I initialize the grid, so I don’t really need to explicitly define grid grid-cols-1 or even grid at all.

The problem, for me, is that I like to use grid-gap to help with spacing between the grid items. It’s just so easy and I don’t have to think about it much. But, I can’t use grid-gap without initializing a grid. So, this is how I end up with Tailwind’s grid utility class on my parent grid div even though I don’t really need it.

As mentioned above, I start with a single column on small screens then move to a two column grid at Tailwind’s md breakpoint. For large screens, 1024 pixels and up, I’ll switch to a three column grid. Finally, on extra-large screens 1280 pixels and above, I’ll end on a four column grid. In this example I’ll use almost all of Tailwind’s breakpoints.

I still end up with this spacing weirdness between sm and md where I think the cards are a little too wide, but this can be fixed with a custom “in between” breakpoint definition in tailwind.config.js. I just didn’t do it for this example.

import React from 'react';
import macbook from './macbook.jpg';
import airtag from './airtag.jpg';
import hand_phone from './hand_phone.jpg';
import dj_booth from './dj_booth.jpg'

const Grid = () => {
  const bodyText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 p-4">
      <Card title="Card 1" subtitle="Subtitle" imgSrc={macbook} imgAlt="macbook" bodyText={bodyText} />
      <Card title="Card 2" subtitle="Subtitle" imgSrc={airtag} imgAlt="airtag" bodyText={bodyText} />
      <Card title="Card 3" subtitle="Subtitle" imgSrc={hand_phone} imgAlt="hand with phone" bodyText={bodyText} />
      <Card title="Card 4" subtitle="Subtitle" imgSrc={dj_booth} imgAlt="dj booth" bodyText={bodyText} />
    </div>
  );
}

Conclusion & final code

Building this responsive grid with Tailwind was a lot less work than I was expecting. Not having to define the media queries was a big time saver for me. I’m still having an internal battle over the use of Tailwind and relenquishing some css control, but I can’t deny that the framework is saving me a ton of time. I still feel more in control of my css with Tailwind than I do with Bootstrap or Material, though.

You can view the full demo here. Try resizing the window to see how the grid responds.

Final code
import React from 'react';
import macbook from './macbook.jpg';
import airtag from './airtag.jpg';
import hand_phone from './hand_phone.jpg';
import dj_booth from './dj_booth.jpg'

const Grid = () => {
  const bodyText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 p-4">
      <Card title="Card 1" subtitle="Subtitle" imgSrc={macbook} imgAlt="macbook" bodyText={bodyText} />
      <Card title="Card 2" subtitle="Subtitle" imgSrc={airtag} imgAlt="airtag" bodyText={bodyText} />
      <Card title="Card 3" subtitle="Subtitle" imgSrc={hand_phone} imgAlt="hand with phone" bodyText={bodyText} />
      <Card title="Card 4" subtitle="Subtitle" imgSrc={dj_booth} imgAlt="dj booth" bodyText={bodyText} />
    </div>
  );
}

const Card = ({ title, subtitle, imgSrc, imgAlt, bodyText }) => (
  <div className="shadow rounded-lg">
    <img className="rounded-tl-lg rounded-tr-lg h-52 w-full" src={imgSrc} alt={imgAlt} />
    <div className="p-2">
      <div className="text-2xl font-semibold">{title}</div>
      <div className="opacity-70">{subtitle}</div>
    </div>
    <div className="px-2 pb-4">
      {bodyText}
    </div>
  </div>
);

export default Grid;