No Time Dad

A blog about web development written by a busy dad

Bootstrap Example Pricing Pricing Page with Tailwindcss

Somehow the idea struck me to try and replicate some of the Bootstrap example templates using Tailwindcss. I thought it would be a fun way to try some new layouts. Honestly, when I looked at their examples I didn’t think they would be complicated to reproduce. I was wrong. They demand more respect than I gave them.

I don’t really remember this from the last time I used Bootstrap, but it has it’s own set of utility classes for handling things like vertical and horizontal spacing, as well as for breakpoints and responsiveness. They actually have names similar to some of Tailwind’s utility classes. I saw names like pt-4, my-5, and border-top.

So, I picked Bootstrap’s example pricing page and started recreating it with Tailwind. What I thought was going to take at most an hour ended up taking several hours. A lot of that time was spent unwinding Bootstrap’s utility classes and re-applying them in Tailwind’s utility classes. I also ended up making a few design concessions, but I think the end product is pretty good.

The final demo can be viewed here. I wrote this example pricing page using React for ease of explanation, but it since it doesn’t use state or event handlers it can be easily translated to another web framework or vanilla html. I should also mention that this isn’t meant to be a tutorial on React. I’m not going to dive into React specific patterns or anything.

Getting started

I think it’s intimidating starting an new page design from scratch. It feels like I have to make important design decisions early that will impact the entire page later. This is somewhat true, but I always have to remind myself that I can change my original struture and design later if needed. Not to say that it’s going to be easy to change, but it can be changed.

Another thing I find helpful early on is to just start getting things on the page. Sometimes I can get stuck in “developers block” and overthink my code. I then end up not writing any code for a while, which sucks.

So, here is the parent component for my Bootstrap pricing page. It’s going to be made up of three sections. A header, a hero, and a content section.

const Index = () => (
  <>
    <Header />
    <Hero />
    <Content />
  </>
)
Header section

The header in the Bootstrap pricing page template has two layouts. One layout for large screens and a second layout for small screens. The large screen layout has all of the header elements in a row, and the small screen switches to a column.

header_large

header_small

I actually really like headers like this. I know the popular trend at the moment is to use dropdown menus on small screens, but I like being able to immediately see the menu and not having to click around for it. Also, this menu doesn’t require anything fancy and it just works.

I think when starting any new element that one of the best things to do is to try to get a rough idea of the tag structure and just add the content. For example, I know that header itself will have a parent container element. The Company name element will need it’s own styles so it’ll have it’s own div. The nav links will change slightly depending on the screen size, so they’ll get their own wrapper too. Lastly, I can see that there is a button (or a) element for signing up.

With that information I have a rough idea of how the header element will look structurally because applying any styles. I’ll start making the component without adding any of Tailwind’s utility classes.

const Header = () => (
  <div>
    <div>Company name</div>
    <div>
      <nav>
        <a href="#link">Features</a>
        <a href="#link">Enterprise</a>
        <a href="#link">Support</a>
        <a href="#link">Pricing</a>
      </nav>
      <button>Sign up</button>
    </div>
  </div>
)

And to no one’s surprise, the component will look nothing like the Bootstrap version pictured above. But, that’s sort of the point. I can now go in and start applying the styles.

The header uses flexbox to control the spacing and direction of the elements inside of it. On small screens, the header should be a column. Once the screen size hits 768px, or Tailwind’s md breakpoint, it should switch to a row.

The nav links should be stacked above the button on small screens, and switch back to both being on a single row on larger screens. Flexbox will take of this for me. I’ll use display: flex via Tailwind’s flex which defaults to flex-direction: row and use md:flex-row to change the direction on the breakpoint.

The remaining styling the header is really just to get the spacing between elements right. Below is the Header component in full.

const Header = () => (
  <div className="flex flex-col items-center text-center px-6 py-3 border-gray-200 border-b-2 md:flex-row md:justify-between">
    <div className="text-lg">Company name</div>
    <div className="md:flex md:items-center md:space-x-6">
      <nav className="space-x-2 md:space-x-4">
        <a className="no-underline text-gray-900 hover:underline" href="#link">
          Features
        </a>
        <a className="no-underline text-gray-900 hover:underline" href="#link">
          Enterprise
        </a>
        <a className="no-underline text-gray-900 hover:underline" href="#link">
          Support
        </a>
        <a className="no-underline text-gray-900 hover:underline" href="#link">
          Pricing
        </a>
      </nav>
      <button className="px-3 py-2 mt-1 border text-sm text-blue-500 border-blue-500 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 hover:bg-blue-500 hover:text-white">
        Sign up
      </button>
    </div>
  </div>
)
Hero section

The hero section was more complicated than I thought it was going to be. The main thing was insuring that the text elements stayed the say width when the screen size reached a certain width. Otherwise, the lines get too thin. This is solved by setting a max-width property on the parent div.

The Bootstrap template uses margin-left: auto; and margin-right: auto; to help center the content on the page. So, I’ll use Tailwind’s mx-auto utility class to take care of that. I’ll also adjust the spacing slighting for larger screens.

const Hero = () => (
  <div className="max-w-3xl text-center mx-auto md:pt-14 p-8">
    <div className="text-5xl py-4">Pricing</div>
    <div className="text-lg font-medium">
      Quickly build an effective pricing table for your potential customers with
      this Bootstrap example. It’s built with default Bootstrap components and
      utilities with little customization.
    </div>
  </div>
)
Content section

This is where I started to diverge from the Bootstrap responsive template. Basically, their template uses flexbox with a combination of flex-wrap and some other related properties to handle the screen size changes.

The problem I found with this approach is that when the screen size hits 768px, the third card drops under the first two cards and stretches across their width. I didn’t love the look of that, so I decided to just use a grid instead and change the column count as the screen size grows.

I ended up having the same issue with the footer section too. The Bootstrap template uses flexbox again there to handle the responsiveness, so I switched it to a grid to make things easier.

The last piece to this section was the pricing cards. I kept my version of them simple, but I think they still look nice. The component itself could be a little more reusable but I think it works well for what it is.

Using grid instead of flexbox definitely saved me a lot of time and headaches here. But, that is only because I’m not quite as comfortable with flexbox’s wrapping and growing utilities as I am with just changing the number of columns in a grid. This is an area I need to work in the future.

const Content = () => (
  <div className="w-full max-w-4xl mx-auto p-4">
    <div className="grid md:grid-cols-3 gap-8 pb-8">
      <PriceCard />
      <PriceCard />
      <PriceCard />
    </div>
    <Footer />
  </div>
)

const PriceCard = () => (
  <div className="flex flex-grow flex-shrink-0 flex-col border border-gray-300 rounded-md">
    <div className="flex items-center justify-center p-2 border-b border-gray-300 bg-gray-100 text-lg">
      Free
    </div>
    <div className="flex flex-col justify-center items-center p-4">
      <div className="text-4xl pb-2">
        $0 <span className="text-2xl text-gray-500">/ mo</span>
      </div>
      <ul className="leading-3 text-sm m-0 py-4">
        <li>10 users include</li>
        <li>2GB of storage</li>
        <li>Email support</li>
        <li>Help center access</li>
      </ul>
      <button className="w-full py-2 border-2 text-sm text-blue-500 border-blue-500 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 hover:bg-blue-500 hover:text-white">
        Sign up for free
      </button>
    </div>
  </div>
)

const Footer = () => (
  <div className="border-t">
    <div className="grid grid-cols-2 gap-4 md:grid-cols-4 py-10">
      <div>
        <FireSvg />
        <div className="text-xs text-gray-500">2017-2019</div>
      </div>
      <div className="">
        <div className="text-lg">Features</div>
        <ul className="leading-none p-0 m-0">
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Cool stuff
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Random feature
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Team feature
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Stuff for developers
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Another one
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Last time
            </a>
          </li>
        </ul>
      </div>
      <div>
        <div className="text-lg">Resources</div>
        <ul className="leading-none p-0 m-0">
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Resourcee
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Resource name
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Another resource
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Final resource
            </a>
          </li>
        </ul>
      </div>
      <div>
        <div className="text-lg">About</div>
        <ul className="leading-none p-0 m-0">
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Team
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Locations
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Privacy
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Terms
            </a>
          </li>
        </ul>
      </div>
    </div>
  </div>
)

const FireSvg = () => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    className="h-8 w-8"
    viewBox="0 0 20 20"
    fill="currentColor"
  >
    <path
      fillRule="evenodd"
      d="M12.395 2.553a1 1 0 00-1.45-.385c-.345.23-.614.558-.822.88-.214.33-.403.713-.57 1.116-.334.804-.614 1.768-.84 2.734a31.365 31.365 0 00-.613 3.58 2.64 2.64 0 01-.945-1.067c-.328-.68-.398-1.534-.398-2.654A1 1 0 005.05 6.05 6.981 6.981 0 003 11a7 7 0 1011.95-4.95c-.592-.591-.98-.985-1.348-1.467-.363-.476-.724-1.063-1.207-2.03zM12.12 15.12A3 3 0 017 13s.879.5 2.5.5c0-1 .5-4 1.25-4.5.5 1 .786 1.293 1.371 1.879A2.99 2.99 0 0113 13a2.99 2.99 0 01-.879 2.121z"
      clipRule="evenodd"
    />
  </svg>
)

Conclusion

This was challenging. I really underestimated how much work recreating the Bootstrap template was going to be. I need to try more of these, maybe next time with vanilla classes.

Final code

The final demo can be viewed here.

// package.json
...
"tailwindcss": "^2.1.2",
"react": "^17.0.2",
...
import React from "react"

const Index = () => (
  <>
    <Header />
    <Hero />
    <Content />
  </>
)

const Header = () => (
  <div className="flex flex-col items-center text-center px-6 py-3 border-gray-200 border-b-2 md:flex-row md:justify-between">
    <div className="text-lg">Company name</div>
    <div className="md:flex md:items-center md:space-x-6">
      <nav className="space-x-2 md:space-x-4">
        <a className="no-underline text-gray-900 hover:underline" href="#link">
          Features
        </a>
        <a className="no-underline text-gray-900 hover:underline" href="#link">
          Enterprise
        </a>
        <a className="no-underline text-gray-900 hover:underline" href="#link">
          Support
        </a>
        <a className="no-underline text-gray-900 hover:underline" href="#link">
          Pricing
        </a>
      </nav>
      <button className="px-3 py-2 mt-1 border text-sm text-blue-500 border-blue-500 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 hover:bg-blue-500 hover:text-white">
        Sign up
      </button>
    </div>
  </div>
)

const Hero = () => (
  <div className="max-w-3xl text-center mx-auto md:pt-14 p-8">
    <div className="text-5xl py-4">Pricing</div>
    <div className="text-lg font-medium">
      Quickly build an effective pricing table for your potential customers with
      this Bootstrap example. It’s built with default Bootstrap components and
      utilities with little customization.
    </div>
  </div>
)

const Content = () => (
  <div className="w-full max-w-4xl mx-auto p-4">
    <div className="grid md:grid-cols-3 gap-8 pb-8">
      <PriceCard />
      <PriceCard />
      <PriceCard />
    </div>
    <Footer />
  </div>
)

const PriceCard = () => (
  <div className="flex flex-grow flex-shrink-0 flex-col border border-gray-300 rounded-md">
    <div className="flex items-center justify-center p-2 border-b border-gray-300 bg-gray-100 text-lg">
      Free
    </div>
    <div className="flex flex-col justify-center items-center p-4">
      <div className="text-4xl pb-2">
        $0 <span className="text-2xl text-gray-500">/ mo</span>
      </div>
      <ul className="leading-3 text-sm m-0 py-4">
        <li>10 users include</li>
        <li>2GB of storage</li>
        <li>Email support</li>
        <li>Help center access</li>
      </ul>
      <button className="w-full py-2 border-2 text-sm text-blue-500 border-blue-500 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 hover:bg-blue-500 hover:text-white">
        Sign up for free
      </button>
    </div>
  </div>
)

const Footer = () => (
  <div className="border-t">
    <div className="grid grid-cols-2 gap-4 md:grid-cols-4 py-10">
      <div>
        <FireSvg />
        <div className="text-xs text-gray-500">2017-2019</div>
      </div>
      <div className="">
        <div className="text-lg">Features</div>
        <ul className="leading-none p-0 m-0">
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Cool stuff
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Random feature
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Team feature
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Stuff for developers
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Another one
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Last time
            </a>
          </li>
        </ul>
      </div>
      <div>
        <div className="text-lg">Resources</div>
        <ul className="leading-none p-0 m-0">
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Resourcee
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Resource name
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Another resource
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Final resource
            </a>
          </li>
        </ul>
      </div>
      <div>
        <div className="text-lg">About</div>
        <ul className="leading-none p-0 m-0">
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Team
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Locations
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Privacy
            </a>
          </li>
          <li className="m-0">
            <a
              className="text-sm no-underline text-gray-500 hover:underline"
              href="#link"
            >
              Terms
            </a>
          </li>
        </ul>
      </div>
    </div>
  </div>
)

const FireSvg = () => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    className="h-8 w-8"
    viewBox="0 0 20 20"
    fill="currentColor"
  >
    <path
      fillRule="evenodd"
      d="M12.395 2.553a1 1 0 00-1.45-.385c-.345.23-.614.558-.822.88-.214.33-.403.713-.57 1.116-.334.804-.614 1.768-.84 2.734a31.365 31.365 0 00-.613 3.58 2.64 2.64 0 01-.945-1.067c-.328-.68-.398-1.534-.398-2.654A1 1 0 005.05 6.05 6.981 6.981 0 003 11a7 7 0 1011.95-4.95c-.592-.591-.98-.985-1.348-1.467-.363-.476-.724-1.063-1.207-2.03zM12.12 15.12A3 3 0 017 13s.879.5 2.5.5c0-1 .5-4 1.25-4.5.5 1 .786 1.293 1.371 1.879A2.99 2.99 0 0113 13a2.99 2.99 0 01-.879 2.121z"
      clipRule="evenodd"
    />
  </svg>
)

export default Index