DEV Community 👩‍💻👨‍💻

Cover image for Styling a payments page with Tailwind CSS
mattling_dev for Stripe

Posted on

Styling a payments page with Tailwind CSS

Introduction

In the previous post in the series, we learned how to enable automatic payment methods for our Payment Element integration allowing us to turn payment methods on and off via the dashboard with a single integration.

In my ongoing #learninpublic quest, I’ll add some basic styling to the checkout component using Tailwind CSS. Tailwind is a utility-first CSS framework that will allow us to style our markup with predefined classes, allowing us to very quickly create consistent styling and design directly in our code.

Follow along

This post builds on the branch 02-automatic-payment-methods in the GitHub repo. If you would like to follow along, checkout that branch using git checkout 02-automatic-payment-methods and apply the changes in this tutorial.

This completed demo is available on GitHub on the branch 03-styling-with-tailwind-css.

Prerequisites

This demo was built using Node version 16.10.0, and npm version 7.24.0. You’ll need a basic understanding of React components and JSX. A basic understanding of CSS will be helpful, although we will not be writing any CSS in this tutorial directly, instead we’ll use Tailwind classes to quickly style our component.

What you’ll learn

In this post you’ll learn how to install Tailwind and its dependencies in a Vite project with React, and the basics of Tailwind CSS to style a checkout page.

Software stack

This project uses Vite as a build and development server, React for a frontend framework, React Stripe to accept payments, and the Payment Element to present and confirm payments on the frontend, and Tailwind to style the page. You can read a deep dive end-to-end integration in the first and second posts.

Installing Tailwind

We’ll start by installing Tailwind and its dependencies:

npm install -D tailwindcss postcss autoprefixer
Enter fullscreen mode Exit fullscreen mode

Then we’ll initialize Tailwind:

npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

This will create two config files for us; tailwind.config.js and postcss.config.js. In tailwind.config.js we’ll add the paths to all of the files that will contain our Tailwind styles:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content:["./src/**/*.{js,jsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Next we’ll create an index.css file for the baseline Tailwind declarations. In the src directory create an index.css file with the following:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

We have to include this css file in our App component:

import Checkout from "./Checkout.jsx";
import './index.css'

function App() {
  return <Checkout />;
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now we can test this by adding some classes to our existing code. In the Checkout component, let’s wrap the Elements provider in a div (JSX requires that adjacent tags be wrapped in a closing tag) and add a h1 above it with some Tailwind classes, in this case we’ll apply a 3xl text size and an orange color:

  return (
    <div>
      {clientSecretSettings.loading ? (
        <h1>Loading ...</h1>
      ) : (
        <div>
          <h1 class="text-3xl text-orange-600">Checkout</h1>
          <Elements
            stripe={stripePromise}
            options={{
              clientSecret: clientSecretSettings.clientSecret,
              appearance: { theme: "stripe" },
            }}
          >
            <CheckoutForm />
          </Elements>
        </div>
      )}
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

We should now see a large orange Checkout heading above the Payment Element.

Orange heading

Order summary

Let’s add a list to reflect some products that we’re selling to our customers. In the Checkout component, we’ll add an order summary component. Firstly, let’s create a simple functional component in the src directory called OrderSummary.jsx that lists the products in the order. In this case, we’re selling a lot of llama related products:

const OrderSummary = () => {
  return(
    <div>
      <h1>Order Summary</h1>
      <ul>
        <li>Llama food x 2</li>
        <li>Llama bedding x 1</li>
        <li>Llama lamps x 3</li>
      </ul>
      <div>
        Total: &euro; 10.00
      </div>
    </div>
  );
};

export default OrderSummary;
Enter fullscreen mode Exit fullscreen mode

Normally we would pass the items as props from the Checkout component, but for the sake of this demo we’ll simply hardcode some list values. Next, we’ll import that in our Checkout component and include it on the page:

import { useEffect, useState } from "react";
import axios from "axios";
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import CheckoutForm from './CheckoutForm.jsx';
import OrderSummary from './OrderSummary.jsx';
Enter fullscreen mode Exit fullscreen mode
  return (
    <div>
      {clientSecretSettings.loading ? (
        <h1>Loading ...</h1>
      ) : (
        <div>
          <OrderSummary />
          <Elements
            stripe={stripePromise}
            options={{
              clientSecret: clientSecretSettings.clientSecret,
              appearance: { theme: "stripe" },
            }}
          >
            <CheckoutForm />
          </Elements>
        </div>
      )}
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

Order summary

We should now see our summary rendering above the payment element.

Layout

Let’s apply some styling to make the layout better. We’d like to have the summary and the payment sheet side by side. For this we’ll use a flexbox and instruct the child items to take up half of the space. The order summary and payment element will be wrapped in their own div:

  return (
    <div class="content-center flex flex-row">
      <div class="basis-1/2">
        <OrderSummary />
      </div>
      <div class="basis-1/2">
        {clientSecretSettings.loading ? (
          <h1 class="font-semibold text-3xl font-sans">Loading ...</h1>
        ) : (
          <Elements
            stripe={stripePromise}
            options={{
              clientSecret: clientSecretSettings.clientSecret,
              appearance: { theme: "stripe" },
            }}
          >
            <CheckoutForm />
          </Elements>
        )}
      </div>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

Layout

Next we’ll add some padding to the component, some background colors to separate the panels and rounded edges. rounded-l means round the left side of the div rounded-r the right side. bg-gray and bg-blue are set to 200 intensity and p-10 adds 40 pixels of padding:

  return (
    <div class="content-center flex flex-row">
      <div class="rounded-l-xl bg-gray-200 basis-1/2 p-10">
        <OrderSummary />
      </div>
      <div class="rounded-r-xl bg-blue-200 basis-1/2 p-10">
        {clientSecretSettings.loading ? (
          <h1 class="font-semibold text-3xl font-sans">Loading ...</h1>
        ) : (
          <Elements
            stripe={stripePromise}
            options={{
              clientSecret: clientSecretSettings.clientSecret,
              appearance: { theme: "stripe" },
            }}
          >
            <CheckoutForm />
          </Elements>
        )}
      </div>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

Add padding and panels

Let’s center the component on the page and add a heading for our Llama store:

  return (
    <div class="mx-auto w-9/12 m-10">
      <h1 class="ml-5 mb-5 font-semibold text-3xl font-sans text-indigo-600">Llama Store</h1>
      <div class="content-center flex flex-row">
        <div class="rounded-l-xl bg-gray-200 basis-1/2 p-10">
          <OrderSummary />
        </div>
        <div class="rounded-r-xl bg-blue-200 basis-1/2 p-10">
          {clientSecretSettings.loading ? (
            <h1 class="font-semibold text-3xl font-sans">Loading ...</h1>
          ) : (
            <Elements
              stripe={stripePromise}
              options={{
                clientSecret: clientSecretSettings.clientSecret,
                appearance: { theme: "stripe" },
              }}
            >
              <CheckoutForm />
            </Elements>
          )}
        </div>
      </div>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

Centered and with title

We have quite a number of classes here so let's take a look. <div class="w-9/12 mx-auto mt-10">. We’ve set the width of the root container to 75% of the screen. The mx-auto utility centers the container, and then we added a top margin of value 10.

For the store heading <h1 class="ml-5 mb-5 font-semibold text-3xl font-sans text-indigo-600">Llama Store</h1> we’ve added some text size classes, a sans serif font, a nice indigo shade and some margins.

Styling the order summary

This is already looking a lot nicer! The order summary is unstyled so let's tackle that. We’ll make the title more prominent, style the list of products with bullets, and move the order total down to separate it from the list:

const OrderSummary = () => {

  return(
    <div>
      <h1 class="mb-5 font-semibold text-2xl font-sans text-indigo-600">Order Summary</h1>
      <ul class=" text-gray-600 font-light list-disc list-inside">
        <li>Llama food x 2</li>
        <li>Llama Bedding x 1</li>
        <li>Llama lamps x 3</li>
      </ul>
      <div class="mt-10 border-gray-300 border-t-2 pt-2 text-gray-600">
        Total: &euro; 10.00
      </div>
    </div>
  );
};

export default OrderSummary;
Enter fullscreen mode Exit fullscreen mode

Styled order summary

Styling the submit button

Finally, let’s style the submit button, in the CheckoutForm component:

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <button class="w-full mt-5 p-2 bg-black text-white rounded-md ">Pay &euro; 10.00</button>
      {/* Show error message to your customers */}
      {errorMessage && <div>{errorMessage}</div>}
    </form>
  )
Enter fullscreen mode Exit fullscreen mode

I’d like the button to take up the full width of the available space to be aligned more with the Payment Element fields. Making it black with white text will also increase its importance on the page and feel more like a call to action.

Styled submit button

Conclusion

We’ve only really skimmed the surface of Tailwind CSS for styling and designing components, but in only a few minutes work we’ve gone from this:

Unstyled checkout

... to this!

Styled checkout

There’s so much more that we could do, but this tutorial shows just how quickly we can style our components using Tailwind CSS. We’re super excited to hear about what you learn and build, so please don’t hesitate to reach out to us anytime!

About the author

Matthew Ling

Matthew Ling (@mattling_dev) is a Developer Advocate at Stripe. Matt loves to tinker with new technology, adores Ruby and coffee and also moonlighted as a pro music photographer. His photo site is at matthewling.com and developer site is at mattling.dev.

Stay connected

In addition, you can stay up to date with Stripe in a few ways:

📣 Follow us on Twitter
💬 Join the official Discord server
📺 Subscribe to our Youtube channel
📧 Sign up for the Dev Digest

Top comments (1)

Collapse
 
beaurowan profile image
BeauRowan

This is different, it's basically a different way of styling your application. Thanks! Spell to turn friendship to love

An Animated Guide to Node.js Event Lop

Node.js doesn’t stop from running other operations because of Libuv, a C++ library responsible for the event loop and asynchronously handling tasks such as network requests, DNS resolution, file system operations, data encryption, etc.

What happens under the hood when Node.js works on tasks such as database queries? We will explore it by following this piece of code step by step.