Build a Simple Next.js Interactive Credit Card Form

In today’s digital landscape, accepting online payments is crucial for businesses of all sizes. Credit card forms are a fundamental part of this process, enabling customers to securely enter their payment information. Building a credit card form might seem daunting at first, but with the right tools and understanding, it can be a straightforward and rewarding project. This article will guide you through creating a simple, interactive credit card form using Next.js, a popular React framework known for its performance and developer-friendly features. We’ll cover everything from setting up your Next.js project to implementing input validation and providing a user-friendly experience.

Why Build a Credit Card Form?

While various payment gateways and third-party services offer ready-made solutions, building your credit card form offers several advantages:

  • Customization: You have complete control over the form’s design and functionality, allowing you to tailor it to your brand and specific needs.
  • Learning: It’s an excellent opportunity to learn about front-end development, data validation, and user interface (UI) design.
  • Control: You manage the data collection process, ensuring compliance with security standards (more on this later).

Building your own form can be a valuable learning experience, even if you eventually integrate a third-party payment gateway. It helps you understand the underlying processes and requirements.

Prerequisites

Before we begin, ensure you have the following:

  • Node.js and npm (or yarn): These are essential for managing project dependencies and running the development server.
  • Basic knowledge of HTML, CSS, and JavaScript: Familiarity with these languages is necessary to understand the code.
  • A code editor: VS Code, Sublime Text, or any other editor you prefer.

Setting Up Your Next.js Project

Let’s start by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app credit-card-form-app
cd credit-card-form-app

This command creates a new Next.js project named “credit-card-form-app”. Navigate into the project directory using `cd credit-card-form-app`. Next, start the development server:

npm run dev

Open your browser and go to `http://localhost:3000`. You should see the default Next.js welcome page. This confirms that your project is set up correctly.

Creating the Credit Card Form Component

We’ll create a reusable component for our credit card form. Create a new file named `CreditCardForm.js` inside the `components` directory (create the `components` directory if it doesn’t exist). Here’s the basic structure of the form:

// components/CreditCardForm.js
import React, { useState } from 'react';

function CreditCardForm() {
  const [cardNumber, setCardNumber] = useState('');
  const [expiryDate, setExpiryDate] = useState('');
  const [cvv, setCvv] = useState('');
  const [cardholderName, setCardholderName] = useState('');
  const [formErrors, setFormErrors] = useState({});

  const handleSubmit = (e) => {
    e.preventDefault();
    // Validation and submission logic will go here
  };

  return (
    <form onSubmit={handleSubmit} className="credit-card-form">
      <div className="form-group">
        <label htmlFor="cardNumber">Card Number</label>
        <input
          type="text"
          id="cardNumber"
          name="cardNumber"
          value={cardNumber}
          onChange={(e) => setCardNumber(e.target.value)}
          placeholder="XXXX XXXX XXXX XXXX"
        />
        {formErrors.cardNumber && <p className="error-message">{formErrors.cardNumber}</p>}
      </div>

      <div className="form-group">
        <label htmlFor="expiryDate">Expiry Date</label>
        <input
          type="text"
          id="expiryDate"
          name="expiryDate"
          value={expiryDate}
          onChange={(e) => setExpiryDate(e.target.value)}
          placeholder="MM/YY"
        />
        {formErrors.expiryDate && <p className="error-message">{formErrors.expiryDate}</p>}
      </div>

      <div className="form-group">
        <label htmlFor="cvv">CVV</label>
        <input
          type="text"
          id="cvv"
          name="cvv"
          value={cvv}
          onChange={(e) => setCvv(e.target.value)}
          placeholder="CVV"
        />
        {formErrors.cvv && <p className="error-message">{formErrors.cvv}</p>}
      </div>

      <div className="form-group">
        <label htmlFor="cardholderName">Cardholder Name</label>
        <input
          type="text"
          id="cardholderName"
          name="cardholderName"
          value={cardholderName}
          onChange={(e) => setCardholderName(e.target.value)}
          placeholder="Cardholder Name"
        />
        {formErrors.cardholderName && <p className="error-message">{formErrors.cardholderName}</p>}
      </div>

      <button type="submit">Submit</button>
    </form>
  );
}

export default CreditCardForm;

This code does the following:

  • Imports `useState` from React: This hook is essential for managing the form’s state.
  • Defines state variables: `cardNumber`, `expiryDate`, `cvv`, and `cardholderName` store the values entered by the user. `formErrors` will hold any validation errors.
  • Creates a `handleSubmit` function: This function will be called when the form is submitted. Currently, it prevents the default form submission behavior.
  • Renders the form: The form includes input fields for each credit card detail and a submit button. Each input has an `onChange` handler that updates the corresponding state variable. Error messages will be displayed below each input field.

Integrating the Form into Your Page

Now, let’s integrate the `CreditCardForm` component into our main page. Open `pages/index.js` and modify it as follows:

// pages/index.js
import CreditCardForm from '../components/CreditCardForm';

function HomePage() {
  return (
    <div className="container">
      <h1>Credit Card Form</h1>
      <CreditCardForm />
    </div>
  );
}

export default HomePage;

This code imports the `CreditCardForm` component and renders it within a container. You can add styling to the `container` class in your `styles/globals.css` file to center the form and improve its appearance.

Adding Basic Styling (CSS)

To make the form look presentable, let’s add some basic CSS. Open `styles/globals.css` and add the following styles. Feel free to customize these to your liking:

/* styles/globals.css */
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
}

.credit-card-form {
  display: flex;
  flex-direction: column;
  width: 300px;
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 5px;
}

.form-group {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

input[type="text"] {
  width: 100%;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 16px;
}

button {
  padding: 10px 20px;
  background-color: #0070f3;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

button:hover {
  background-color: #0056b3;
}

.error-message {
  color: red;
  font-size: 14px;
  margin-top: 5px;
}

This CSS provides basic styling for the form, input fields, labels, and the submit button. It also includes styling for error messages, which we’ll implement next.

Implementing Input Validation

Input validation is crucial to ensure that the user enters valid credit card details. Let’s add validation to our form. We’ll use regular expressions (regex) and basic checks to validate the card number, expiry date, and CVV. Modify the `handleSubmit` function in `CreditCardForm.js` as follows:

// components/CreditCardForm.js
import React, { useState } from 'react';

function CreditCardForm() {
  // ... (state variables)

  const handleSubmit = (e) => {
    e.preventDefault();
    const errors = {};

    // Card Number Validation
    if (!cardNumber) {
      errors.cardNumber = 'Card number is required';
    } else if (!/^[0-9]{13,19}$/.test(cardNumber)) {
      errors.cardNumber = 'Invalid card number';
    }

    // Expiry Date Validation
    if (!expiryDate) {
      errors.expiryDate = 'Expiry date is required';
    } else if (!/^(0[1-9]|1[0-2])/([0-9]{2})$/.test(expiryDate)) {
      errors.expiryDate = 'Invalid expiry date (MM/YY)';
    }

    // CVV Validation
    if (!cvv) {
      errors.cvv = 'CVV is required';
    } else if (!/^[0-9]{3,4}$/.test(cvv)) {
      errors.cvv = 'Invalid CVV';
    }

    // Cardholder Name Validation (Optional)
    if (!cardholderName) {
      errors.cardholderName = 'Cardholder name is required';
    }

    setFormErrors(errors);

    if (Object.keys(errors).length === 0) {
      // Form is valid, submit data (replace with your submission logic)
      alert('Form submitted successfully!');
      console.log('Form Data:', { cardNumber, expiryDate, cvv, cardholderName });
    }
  };

  return (
    // ... (form rendering)
  );
}

export default CreditCardForm;

Here’s a breakdown of the validation logic:

  • Creates an `errors` object: This object will store any validation errors.
  • Validates `cardNumber`: Checks if it’s empty and if it matches the regex for a valid credit card number (13-19 digits).
  • Validates `expiryDate`: Checks if it’s empty and if it matches the regex for the MM/YY format.
  • Validates `cvv`: Checks if it’s empty and if it contains 3 or 4 digits.
  • Validates `cardholderName`: Checks if it’s empty.
  • Sets `formErrors`: Updates the `formErrors` state with the validation errors.
  • Submits the form (conditionally): If there are no errors ( `Object.keys(errors).length === 0`), it displays an alert (replace this with your actual submission logic).

Now, when the user submits the form, the validation checks will run, and error messages will be displayed below the corresponding input fields if any errors are found.

Enhancing the User Experience

Let’s add some features to improve the user experience:

  • Real-time validation feedback: Provide immediate feedback as the user types, rather than waiting for the form submission.
  • Input formatting: Automatically format the card number and expiry date as the user types.
  • Card type detection: Display an icon representing the card type (Visa, Mastercard, etc.).

Real-time Validation

To provide real-time validation, we can add an `onBlur` event handler to each input field. This will trigger the validation when the user leaves the input field. Modify the input fields in `CreditCardForm.js` to include the `onBlur` event:


<input
  type="text"
  id="cardNumber"
  name="cardNumber"
  value={cardNumber}
  onChange={(e) => setCardNumber(e.target.value)}
  onBlur={() => validateField('cardNumber', cardNumber)}
  placeholder="XXXX XXXX XXXX XXXX"
/>

We’ll also need to create a `validateField` function to handle the validation logic. Add this function inside the `CreditCardForm` component:


const validateField = (fieldName, value) => {
  const errors = { ...formErrors }; // Copy existing errors

  switch (fieldName) {
    case 'cardNumber':
      if (!value) {
        errors.cardNumber = 'Card number is required';
      } else if (!/^[0-9]{13,19}$/.test(value)) {
        errors.cardNumber = 'Invalid card number';
      } else {
        delete errors.cardNumber; // Clear error if valid
      }
      break;
    case 'expiryDate':
      if (!value) {
        errors.expiryDate = 'Expiry date is required';
      } else if (!/^(0[1-9]|1[0-2])/([0-9]{2})$/.test(value)) {
        errors.expiryDate = 'Invalid expiry date (MM/YY)';
      } else {
        delete errors.expiryDate;
      }
      break;
    case 'cvv':
      if (!value) {
        errors.cvv = 'CVV is required';
      } else if (!/^[0-9]{3,4}$/.test(value)) {
        errors.cvv = 'Invalid CVV';
      } else {
        delete errors.cvv;
      }
      break;
    case 'cardholderName':
      if (!value) {
        errors.cardholderName = 'Cardholder name is required';
      } else {
        delete errors.cardholderName;
      }
      break;
    default:
      break;
  }

  setFormErrors(errors);
};

This `validateField` function takes the field name and its value as arguments. It then performs the validation logic for the specific field and updates the `formErrors` state accordingly. The `delete errors.fieldName` lines clear the error message if the field is valid.

Input Formatting

Let’s add input formatting for the card number and expiry date to improve usability. We can use libraries like `cleave.js` or implement custom formatting functions. Here’s an example using a custom formatting function for the card number:

// components/CreditCardForm.js
const formatCardNumber = (value) => {
  // Remove non-numeric characters
  const cleanedValue = value.replace(/[^0-9]/g, '');
  let formattedValue = '';

  for (let i = 0; i < cleanedValue.length; i++) {
    if (i > 0 && i % 4 === 0) {
      formattedValue += ' '; // Add space after every 4 digits
    }
    formattedValue += cleanedValue[i];
  }

  return formattedValue;
};

Modify the `onChange` handler for the card number input to use the `formatCardNumber` function:


<input
  type="text"
  id="cardNumber"
  name="cardNumber"
  value={formatCardNumber(cardNumber)}
  onChange={(e) => setCardNumber(e.target.value)}
  onBlur={() => validateField('cardNumber', cardNumber)}
  placeholder="XXXX XXXX XXXX XXXX"
/>

For the expiry date, you can use a similar approach or a library like `cleave.js` to automatically format the input as MM/YY. You’ll need to install `cleave.js` using `npm install cleave.js`. Then, import it and apply it to the expiry date input.


import Cleave from 'cleave.js/react';
import 'cleave.js/dist/addons/cleave-phone.us';

<input
    type="text"
    id="expiryDate"
    name="expiryDate"
    value={expiryDate}
    onChange={(e) => setExpiryDate(e.target.value)}
    onBlur={() => validateField('expiryDate', expiryDate)}
    placeholder="MM/YY"
    as={Cleave}
    options={{
        date: true,
        datePattern: ['m', 'y'],
    }}
/>

Card Type Detection

Card type detection can enhance the user experience. You can use a library like `credit-card-type` or implement your own logic to detect the card type based on the card number’s prefix. Here’s a simplified example:


// components/CreditCardForm.js
import React, { useState, useEffect } from 'react';
//... (other imports)

function CreditCardForm() {
  const [cardNumber, setCardNumber] = useState('');
  const [expiryDate, setExpiryDate] = useState('');
  const [cvv, setCvv] = useState('');
  const [cardholderName, setCardholderName] = useState('');
  const [formErrors, setFormErrors] = useState({});
  const [cardType, setCardType] = useState(null);

  useEffect(() => {
    const detectCardType = () => {
      if (!cardNumber) {
        setCardType(null);
        return;
      }

      const cleanedNumber = cardNumber.replace(/s/g, '');
      if (/^4[0-9]{12}(?:[0-9]{3})?$/.test(cleanedNumber)) {
        setCardType('visa');
      } else if (/^5[1-5][0-9]{14}$/.test(cleanedNumber)) {
        setCardType('mastercard');
      } else if (/^3[47][0-9]{13}$/.test(cleanedNumber)) {
        setCardType('amex');
      } else if (/^6(?:011|5[0-9]{2})[0-9]{12}$/.test(cleanedNumber)) {
        setCardType('discover');
      } else {
        setCardType('unknown');
      }
    };

    detectCardType();
  }, [cardNumber]);

  return (
    <form onSubmit={handleSubmit} className="credit-card-form">
      <div className="form-group">
        <label htmlFor="cardNumber">Card Number</label>
        <input
          type="text"
          id="cardNumber"
          name="cardNumber"
          value={formatCardNumber(cardNumber)}
          onChange={(e) => setCardNumber(e.target.value)}
          onBlur={() => validateField('cardNumber', cardNumber)}
          placeholder="XXXX XXXX XXXX XXXX"
        />
        {formErrors.cardNumber && <p className="error-message">{formErrors.cardNumber}</p>}
      </div>

      <div className="form-group">
        <label htmlFor="expiryDate">Expiry Date</label>
        <input
          type="text"
          id="expiryDate"
          name="expiryDate"
          value={expiryDate}
          onChange={(e) => setExpiryDate(e.target.value)}
          onBlur={() => validateField('expiryDate', expiryDate)}
          placeholder="MM/YY"
        />
        {formErrors.expiryDate && <p className="error-message">{formErrors.expiryDate}</p>}
      </div>

      <div className="form-group">
        <label htmlFor="cvv">CVV</label>
        <input
          type="text"
          id="cvv"
          name="cvv"
          value={cvv}
          onChange={(e) => setCvv(e.target.value)}
          onBlur={() => validateField('cvv', cvv)}
          placeholder="CVV"
        />
        {formErrors.cvv && <p className="error-message">{formErrors.cvv}</p>}
      </div>

      <div className="form-group">
        <label htmlFor="cardholderName">Cardholder Name</label>
        <input
          type="text"
          id="cardholderName"
          name="cardholderName"
          value={cardholderName}
          onChange={(e) => setCardholderName(e.target.value)}
          onBlur={() => validateField('cardholderName', cardholderName)}
          placeholder="Cardholder Name"
        />
        {formErrors.cardholderName && <p className="error-message">{formErrors.cardholderName}</p>}
      </div>

      <div className="form-group">
        {cardType && (
          <img
            src={`/images/${cardType}.png`}  // Assuming you have card type images
            alt={`${cardType} card`}
            style={{ width: '50px', height: '30px' }}
          />
        )}
      </div>

      <button type="submit">Submit</button>
    </form>
  );
}

export default CreditCardForm;

In this example, we’re using a `useEffect` hook to detect the card type whenever the `cardNumber` changes. The `detectCardType` function checks the card number against regular expressions for common card types (Visa, Mastercard, Amex, Discover). You’ll need to create image files for each card type (e.g., `visa.png`, `mastercard.png`) and place them in the `public/images` directory.

Remember to install the necessary packages. For example:

npm install cleave.js

Important Security Considerations

When handling credit card information, security is paramount. Here are some crucial security considerations:

  • Never store sensitive data: Do not store credit card numbers, CVV, or expiry dates on your server. This is a major security risk and violates PCI DSS compliance.
  • Use HTTPS: Ensure your website uses HTTPS (SSL/TLS) to encrypt the data transmitted between the user’s browser and your server.
  • PCI DSS Compliance: If you’re directly processing credit card information, you must comply with the Payment Card Industry Data Security Standard (PCI DSS). This involves rigorous security measures, including regular security audits, firewalls, and data encryption. Consider using a third-party payment gateway to handle the sensitive data and reduce your PCI DSS scope.
  • Input validation: Implement robust input validation to prevent invalid data from being submitted.
  • Tokenization: Use tokenization to replace sensitive credit card data with a unique, non-sensitive identifier (token). This allows you to process payments without storing the actual card details.
  • Third-party payment gateways: Consider using established payment gateways like Stripe, PayPal, or Braintree. They handle the secure storage and processing of credit card information, simplifying your PCI DSS compliance.
  • Server-side processing: All sensitive data processing must happen server-side, never in the client-side code.

The example code provided in this article is for educational purposes only and is not intended for production use without careful consideration of security best practices. Always prioritize security when dealing with sensitive financial data.

Deployment

Deploying your Next.js application is relatively straightforward. Here are a few popular options:

  • Vercel: Vercel is the platform created by the creators of Next.js and offers seamless deployment and hosting. It’s the easiest and recommended option for Next.js apps.
  • Netlify: Netlify is another excellent platform for deploying web applications, offering continuous deployment and other features.
  • Other hosting providers: You can also deploy your Next.js app to other platforms like AWS, Google Cloud, or Azure. However, these often require more configuration.

To deploy to Vercel, for example, you can simply run the following command in your terminal, after initializing a git repository:

npx vercel

Vercel will guide you through the deployment process.

Common Mistakes and How to Fix Them

Here are some common mistakes when building credit card forms and how to avoid them:

  • Storing sensitive data: Never store credit card numbers, CVV, or expiry dates on your server. Use a secure payment gateway or tokenization.
  • Lack of input validation: Always validate user input to prevent errors and ensure data integrity.
  • Poor user experience: Make the form user-friendly with clear labels, helpful error messages, and input formatting.
  • Ignoring security best practices: Follow security best practices, including using HTTPS, PCI DSS compliance (if applicable), and server-side processing.
  • Not testing thoroughly: Test your form with different credit card numbers, expiry dates, and CVVs to ensure it works correctly. Also, test the form on different browsers and devices.
  • Using client-side validation only: Always perform server-side validation as well, as client-side validation can be bypassed.

Summary / Key Takeaways

Building a credit card form in Next.js involves creating a component with input fields, implementing input validation, and handling form submission. Remember that security is paramount when dealing with sensitive financial data. Prioritize security best practices, such as not storing sensitive data, using HTTPS, and complying with PCI DSS (if applicable). Consider using a third-party payment gateway to handle the secure storage and processing of credit card information. By following these steps and incorporating the enhancements discussed, you can create a functional and user-friendly credit card form in your Next.js application. This project not only equips you with the skills to handle online payments but also reinforces the importance of secure coding practices in web development.