Tailwind CSS: Creating a simple and modern Payment Card Form

Tailwind CSS: Creating a simple and modern Payment Card Form

In this comprehensive blog post, I'll guide you through the process of building a payment card form using Tailwind CSS.

Navigating the landscape of pre-built Tailwind components can be challenging, and that's where this tutorial comes in — providing you with a customizable, plug-and-play payment card form designed to seamlessly enhance your web application's checkout experience.

What will we build? Take a sneak peek at the final result and view the code here.

1. Getting started with Tailwind CSS in a non-framework environment

Getting started with Tailwind CSS in a non-framework environment is a breeze. Follow these simple steps to integrate Tailwind into your project seamlessly:

  • First, make sure you have Node.js installed on your machine.

  • Then, install Tailwind CSS and its dependencies using npm.

      npm install -D tailwindcss
      npx tailwindcss init
    
  • Once installed, create `tailwind.config.js` file to tailor Tailwind to your project's needs. For my project I used the following config file:

      /** myProject/tailwind.config.js */
      module.exports = {
        content: ["./src/**/*.{html,js}"],
        theme: {
          extend: {},
        },
        plugins: [],
      };
    
  • Add Tailwind to your CSS:

      /* src/style.css */
      @tailwind base;
      @tailwind components;
      @tailwind utilities;
    
  • Start the Tailwind build process in your terminal:

      npx tailwindcss -i ./src/style.css -o ./dist/style.css --watch
    
  • Add the path for the generated style file in your html and you can now start using Tailwind's utility classes to effortlessly style your components:

      <html>
      <head>
        ...
        <link href="/dist/style.css" rel="stylesheet">
      </head>
      <body>
        <!-- start using Tailwind classes -->
        <main class="flex min-h-screen bg-slate-400"></main>
      </body>
      </html>
    
  • The official Tailwind CSS documentation provides extra guides tailored to your specific framework or environment.

2. Component structure

2.1 Container:

  • Start with a container to hold the entire form:

          <main>
            <form>
              <label>Card number:</label>
              <input type="text" />
              <label>Exp. date:</label>
              <input type="text" />
              <label>CCV:</label>
              <input type="text" />
              <label>Card holder:</label>
              <input type="text" />
            </form>
          </main>
    
  • Add some basic style for the main element to have

    • the minimum height of the screen: min-h-screen

    • a padding: p-24

    • a background color: bg-slate-100

    • and to center the form inside: flex flex-col items-center justify-between

            <main class="min-h-screen p-24 bg-slate-100 flex flex-col items-center justify-between">
              ...
            </main>
      
  • Style the form element to have:

    • a white background: bg-white

    • full width within its container: w-full

    • a maximum width on larger screens: max-w-3xl (max-width: 48rem/* 768px */), mx-auto

    • a padding base: px-6 py-8

    • a shadow: shadow-md

    • rounded corners: rounded-md

    • adjusts the layout using flexbox: flex flex-row

        <form class="bg-white w-full max-w-3xl mx-auto px-6 py-8 shadow-md rounded-md">
        ...
        </form>
      

2.2 Inputs:

  • The style for the input elements will have:

    • a flexible layout: flex

    • specific height and a full width: h-10 w-full

    • rounded corners: rounded-md

    • borders: border-2

    • padding: px-4 py-1.5

    • and various focus and disabled states for enhanced user interaction and accessibility: ring-offset-background focus-visible:outline-none focus-visible:border-purple-600 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50.

        <input type="text" 
            class="
                flex h-10 w-full 
                rounded-md border-2 
                px-4 py-1.5 text-lg 
                ring-offset-background 
                focus-visible:outline-none focus-visible:border-purple-600 focus-visible:ring-ring focus-visible:ring-offset-2 
                disabled:cursor-not-allowed disabled:opacity-50
            " 
        />
      

2.3 Labels:

  • Apart from this styles, we'll style the label to have:

    • a specific color: text-neutral-800

    • bold font weight: font-bold

    • a small font size: text-sm

    • margin at the bottom: mb-2

    • make the element a block-level element in the layout: block

        <label class="text-neutral-800 font-bold text-sm mb-2 block">
            Card number:
        </label>
      

2.4 Form layout:

  • For the exp date and ccv input we'll:

    • define a flex container: flex

    • with a horizontal gap between its children: gap-x-2

    • and add margin at the bottom of the container: mb-4

  • Apart from this, we'll wrap each label and input in a child div with flex-1 which makes them take up equal width and fill the available space within the flex container.

  • This combination is useful for creating a spaced-out and visually appealing layout using flexbox.

      <div class="flex gap-x-2 mb-4">
          <div class="flex-1">
              <label class="...">Exp. date:</label>
              <input class="..." type="text" />
          </div>
          <div class="flex-1">
              <label class="...">CCV:</label>
              <input class="..." type="text" />
          </div>
      ...
      </div>
    
  • So far, nothing unreasonable. Our form should look like this:

3. Interactive Elements

For this part we'll add a card image on the right part of our form and use it for some interactivity. The goal is to introduce a visually appealing and smooth 3D animation to enhance the user experience.

3.1 The new two-column form layout:

  • To initiate, let's establish a two-column layout within our form element, demarcated by a vertical border between the columns. Achieving this involves adding flex class to the form element and styling two child divs with the w-1/2 class, meaning that each child is allocated 50% of the width.

  • The pr-8 and pl-8 classes add padding to create space between the two columns, and the border-r-2 and border-slate-300 classes add a vertical border between them.

      <form class="... flex">
          <div class="w-1/2 pr-8 border-r-2 border-slate-300">
              ...
          </div>
          <div class="w-1/2 pl-8">
              <!-- this will be our image wrapper -->
          </div>
      </form>
    

3.2 The image element wrappers:

  • We'll then create an element that will have:

    • a full-width: w-full

    • a fixed height of 14rem / 224px: h-56

    • and a 3D perspective effect applied: style="perspective: 1000px"

  • Regrettably, Tailwind CSS lacks a dedicated class for perspective, necessitating the inclusion of this styling aspect through an inline style. This will help us in the next steps for our 3D animation.

  • And for the second wrapper:

    • position relative to its normal position: relative

    • a pointer cursor: cursor-pointer

    • includes a transition effect on the transform property: transition-transform

    • with a 500ms duration: duration-500

    • and preserves 3D transformations for its child elements: style="transform-style: preserve-3d"

        <form class="...">
            <div class="w-1/2 pr-8 border-r-2 border-slate-300">
                ...
            </div>
            <div class="w-1/2 pl-8">
                <!-- this will be our image wrapper -->
                <div class="w-full h-56" style="perspective: 1000px">
                    <div
                      class="relative cursor-pointer transition-transform duration-500"
                      style="transform-style: preserve-3d"
                    >
                    <!-- this will be our card images: 
                              one for the back and for the front of our card -->
                    </div>
                </div>
            </div>
        </form>
      

3.3 The image wrapper element:

  • Here, we'll create a centered element with:

    • rounded corners: rounded-xl

    • a shadow for a card-like appearance shadow-2xl

    • a style to ensure that the back face of the element is not visible during any 3D transformations. style="backface-visibility: hidden"

    • Inside this container, we'll include an image featuring the rear side of our card.

        <!-- this will be our card images: 
        one for the front-facing side and one for the rear side of our card -->
        <!-- The front-facing side of the card -->
        <div class="w-full m-auto rounded-xl shadow-2xl absolute"
             style="backface-visibility: hidden">
                     <img src="https://i.ibb.co/B2vQ0xG/Card-1.jpg" />
        </div>
        <!-- the rear side of the card -->
        <div class="w-full m-auto rounded-xl shadow-2xl absolute"
             style="backface-visibility: hidden; transform: rotateY(180deg)">
                     <img src="https://i.ibb.co/ThGc8mn/Card-2.jpg" />
        </div>
      
  • In the following step, we'll introduce additional custom CSS to implement the next styling rule:

      <style>
            .crediCard:hover {
              transform: rotateY(-180deg);
            }
      </style>
    

    Once more, since Tailwind doesn't offer a class for transform: rotateY, we'll need to include this transformation in-line.

  • Up to this point, we've implemented a foundational aspect of our interactivity through the card flip animation:

4. Go next level with the animation

Our goal in this step is to dynamically update the values on the card image as we type in our form.

4.1 Transform the text from the image into HTML components

To achieve this, we'll utilize different background images for our card elements, allowing us to update the text dynamically.

Returning to our code:

  • Update the images used as background with the following classes:

    • for position the image relative to its normal position in the document flow: relative

    • to ensure that the image is proportionally scaled to cover the entire container: object-cover w-full h-full

    • to apply rounded corners with an extra-large border-radius: rounded-xl

        <img src="..." class="relative object-cover w-full h-full rounded-xl"/>
      
  • We'll also introduce a positioned absolute div over our background image, responsible for wrapping our card details elements such as card number, expiration date, and more.

  • For this wrapper we'll add the following classes that creates a full-width element with padding on the left and right sides, positioned absolutely 8 units from the top and sets the text color to white:

      <!-- the front side of the card -->
      <img src="..." class="relative object-cover w-full h-full rounded-xl"/>
      <div class="w-full px-8 absolute top-8 text-white">
       ...
      </div>
    
  • Next, we'll create the elements for card number, name and exp date that can be updated by the user on the front of the card:

      <div class="w-full px-8 absolute top-8 text-white">
        <div class="pt-1">
          <p class="font-light">Card Number</p>
          <p id="imageCardNumber" class="font-medium tracking-more-wider h-6">
            4256 4256 4256 4256
          </p>
        </div>
        <div class="pt-6 flex justify-between">
          <div>
            <p class="font-light">Name</p>
            <p id="imageCardName" class="font-medium tracking-widest h-6">John Doe</p>
          </div>
          <div>
            <p class="font-light">Expiry</p>
            <p id="imageExpDate" class="font-medium tracking-wider h-6 w-14">12/24</p>
          </div>
        </div>
      </div>
    
  • We've incorporated IDs into our elements to facilitate targeting them with JavaScript in subsequent steps.

  • And the same for the element on the rear side of the card image:

      <!-- the rear side of the card -->
      <img src="..." class="relative object-cover w-full h-full rounded-xl"/>
      <div class="w-full absolute top-8">
        <div class="px-8 mt-12">
          <p
            id="imageCCVNumber"
            class="text-black flex items-center pl-4 pr-2 w-14 ml-auto"
          >
            342
          </p>
          <p class="text-white font-light flex justify-end text-sm mt-2">
            security code
          </p>
        </div>
      </div>
    
  • Until now, our card component is prepared to dynamically update its values based on user inputs:

    %[codepen.io/schinteiedavid/pen/xxMNeGy]

  • As you can notice our animation is a bit glitchy but this we'll be handled in our next step.

4.2 Add some JS magic:

For this section we'll add some JS for validating the inputs values and to dynamically update the values from the card image element

First, we'll add a function that we'll handle triggering the flip animation:

  const cardEl = document.getElementById("creditCard");
  const flipCard = (flip) => {
    if (flip === "flipToRear" && !cardEl.classList.contains("rearIsVsible")) {
      cardEl.classList.add("rearIsVsible");
    }

    if (flip === "flipToFront" && cardEl.classList.contains("rearIsVsible")) {
      cardEl.classList.remove("rearIsVsible");
    }

    if (flip === "flip") {
      if (cardEl.classList.contains("rearIsVsible")) {
        cardEl.classList.remove("rearIsVsible");
      } else {
        cardEl.classList.add("rearIsVsible");
      }
    }
  };

For that to work, we'll update our styling to trigger the flip animation on a specific class and not on hover:

<style>
  .crediCard.rearIsVsible {
    transform: rotateY(-180deg);
  }
</style>

Also, we'll add the following html attributes to the existing input elements:

  • id="cardNumber" - provides a unique identifier for the input element, making it easier to reference and manipulate using our JS scripts.

  • onclick="flipCard('flipToFront')" - defines the flipCard function to be executed when the input is clicked. In this case, it triggers the function with the argument 'flipToFront'. This will trigger flipping the card element to front if the card is on the rear side.

  • maxlength="19" - specifies the maximum number of characters allowed in the input. In this case, it limits the input to 19 characters, which is often used for credit card numbers.

  • placeholder="XXXX XXXX XXXX XXXX" - displays a temporary hint inside the input field, providing an example or format for the expected input. In this case, it suggests the format for a credit card number.

  • value="4256 4256 4256 4256" - sets the initial value of the input field. In this example, it pre-fills the input with a sample credit card number.

<!-- Card number input -->
<label class="...">Card number:</label>
<input
  type="text"
  class="..."
  id="cardNumber"
  onclick="flipCard('flipToFront')"
  maxlength="19"
  placeholder="XXXX XXXX XXXX XXXX"
  value="4256 4256 4256 4256"
/>
...
<!-- Exp. date input -->
<label class="...">Exp. date:</label>
<input
  type="text"
  class="..."
  id="expDate"
  onclick="flipCard('flipToFront')"
  maxlength="5"
  placeholder="MM/YY"
  value="12/24"
/>
...
<!-- CCV input -->
<label class="...">CCV:</label>
<input
  type="text"
  class="..."
  id="ccvNumber"
  onclick="flipCard('flipToRear')"
  maxlength="3"
  placeholder="123"
  value="342"
/>
...
<!-- Card holder input -->
<label class="...">Card holder:</label>
<input
  type="text"
  class="..."  
  id="cardName"
  onclick="flipCard('flipToFront')"
  placeholder="John Doe"
  value="John Doe"
/>

Next, we will verify user inputs through the utilization of the following JavaScript functions:

// Handle Card Number update
const inputCardNumber = document.getElementById("cardNumber");

inputCardNumber.addEventListener("input", (event) => {
  //   Remove all non-numeric characters from the input
  const input = event.target.value.replace(/\D/g, "");

  // Add a space after every 4 digits
  let formattedInput = "";
  for (let i = 0; i < input.length; i++) {
    if (i % 4 === 0 && i > 0) {
      formattedInput += " ";
    }
    formattedInput += input[i];
  }

  inputCardNumber.value = formattedInput;
});

// Handle CCV update
const inputCCVNumber = document.getElementById("ccvNumber");

inputCCVNumber.addEventListener("input", (event) => {
  // Remove all non-numeric characters from the input
  const input = event.target.value.replace(/\D/g, "");
  inputCCVNumber.value = input;
});

// Handle Exp Date update
const expirationDate = document.getElementById("expDate");

expirationDate.addEventListener("input", (event) => {
  // Remove all non-numeric characters from the input
  const input = event.target.value.replace(/\D/g, "");

  // Add a '/' after the first 2 digits
  let formattedInput = "";
  for (let i = 0; i < input.length; i++) {
    if (i % 2 === 0 && i > 0) {
      formattedInput += "/";
    }
    formattedInput += input[i];
  }

  expirationDate.value = formattedInput;
});

The final component for our puzzle involves incorporating the input values into the card elements.

To achieve this, we will include the following in our current JavaScript script:

// Handle Card Number update
...
const imageCardNumber = document.getElementById("imageCardNumber");

inputCardNumber.addEventListener("input", (event) => {
  ...
  imageCardNumber.innerHTML = formattedInput;
});

// Handle CCV update
...
const imageCCVNumber = document.getElementById("imageCCVNumber");

inputCCVNumber.addEventListener("input", (event) => {
  ...
  imageCCVNumber.innerHTML = input;
});

// Handle Exp Date update
...
const imageExpDate = document.getElementById("imageExpDate");

expirationDate.addEventListener("input", (event) => {
  ...
  imageExpDate.innerHTML = formattedInput;
});


// Handle Card Name update
const inputCardName = document.getElementById("cardName");
const imageCardName = document.getElementById("imageCardName");

inputCardName.addEventListener("input", (event) => {
  imageCardName.innerHTML = event.target.value;
});

The grand finale:

In conclusion, I hope this Tailwind CSS tutorial has been a helpful guide in enhancing your web development skills. If you're eager to see the concepts in action, don't forget to check out the live demo with the accompanying code here. Explore and experiment with the provided code to solidify your understanding and feel free to reach out with any questions or feedback.

Happy coding! 👨‍💻