Build a credit card form with Vue.js

Build a credit card form with Vue.js

In this tutorial, you will learn how to build a modern credit card form component using Vue.js.

Some neat features

  • Auto-rotate card on hover and when switching input.
  • two-way binding between inputs and card UI.
  • Auto-detect type of card, switch card icon.
  • Restrict user from entering nonvalid formats.
  • Dynamic backgrounds.

Demo

Can be found here.

Setup

I have created a starter repo with vue.js and all the assets we will be using. If you want to get the final source code it's available in the master branch. Open up a terminal and execute the following commands.

git clone https://github.com/patni1992/vue-credit-card.git
cd vue-credit-card-form
git checkout starter-code
npm install

Dependencies

Except for vue.js we only have vue-imask as an extra dependency. This package will make sure inputs are formatted in the right way.

Creating our form component

Create a folder called components in src and create CreditCardForm.vue with the following content.

// src/components/CreditCardForm.vue
<template>
  <div class="card-form">
    <div class="card-form__inner">
      <div class="card-input">
        <label for="cardNumber" class="card-input__label">
          Card Number
        </label>
        <input
          :value="cardNumber"
          autofocus
          id="cardNumber"
          class="card-input__input"
          autocomplete="off"
        />
      </div>
      <div class="card-input">
        <label for="cardName" class="card-input__label">
          Card Owner
        </label>
        <input
      
          id="cardName"
          class="card-input__input"
          v-model="name"
          autocomplete="off"
        />
      </div>
      <div class="card-form__row">
        <div class="card-form__col">
          <div class="card-form__group">
            <label for="cardMonth" class="card-input__label">
              Expiration Date
            </label>
            <select
              class="card-input__input -select"
              id="cardMonth"
              v-model="expireMonth"
            >
              <option value="" disabled selected>Month</option>
              <option
                v-for="n in 12"
                :value="n < 10 ? '0' + n : n"
                :key="n"
              >
                {{ 10 > n ? "0" + n : n }}
              </option>
            </select>
            <select
              class="card-input__input -select"
              id="cardYear"
              v-model="expireYear"
            >
              <option value="" disabled selected>Year</option>
              <option
                v-for="(n, $index) in 12"
                :value="$index + currentYear"
                :key="n"
              >
                {{ $index + currentYear }}
              </option>
            </select>
          </div>
        </div>
        <div class="card-form__col -cvv">
          <div class="card-input">
            <label for="cardCvv" class="card-input__label">CVV</label>
            <input
          
              class="card-input__input"
              id="cardCvv"
              :value="cvv"
              autocomplete="off"
            />
          </div>
        </div>
      </div>
      <button @click="submitCard" class="card-form__button">
        Submit
      </button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      cardNumber: "5373 8172 9406 5052",
      expireMonth: "",
      expireYear: "",
      name: "",
      cvv: "",
      currentYear: new Date().getFullYear(),
    };
  },
  methods: {
    submitCard() {
      alert(`
        ${this.cardNumber}\n
        ${this.name}\n
        ${this.expireMonth}/${this.expireYear}\n
        ${this.cvv}`);
    },
  },
};
</script>

<style scoped lang="scss">
.card-container {
  margin: 30px auto 50px auto;
}

.card-form {
  max-width: 570px;
  margin: auto;
  width: 100%;

  &__inner {
    background: #fff;
    box-shadow: 0 30px 60px 0 rgba(90, 116, 148, 0.4);
    border-radius: 10px;
    padding: 20px;
  }

  &__row {
    display: flex;
    align-items: flex-start;
  }

  &__col {
    flex: auto;
    margin-right: 15px;

    &:last-child {
      margin-right: 0;
    }

    &.-cvv {
      max-width: 150px;
    }
  }

  &__group {
    display: flex;
    align-items: flex-start;
    flex-wrap: wrap;

    .card-input__input {
      flex: 1;
      margin-right: 15px;

      &:last-child {
        margin-right: 0;
      }
    }
  }

  &__button {
    width: 100%;
    height: 55px;
    background: #38a294;
    border: none;
    border-radius: 5px;
    font-size: 22px;
    font-weight: 500;
    box-shadow: 3px 10px 20px 0px rgba(35, 100, 210, 0.3);
    color: #fff;
    margin-top: 20px;
    cursor: pointer;

    &:hover {
      background: darken(#38a294, 5%);
    }
  }
}

.card-input {
  margin-bottom: 20px;
  &__label {
    margin-bottom: 5px;
    color: #1a3b5d;
    width: 100%;
    display: block;
    text-align: left;
  }
  &__input {
    width: 100%;
    height: 50px;
    border-radius: 5px;
    box-shadow: none;
    border: 1px solid #ced6e0;
    transition: all 0.3s ease-in-out;
    font-size: 18px;
    padding: 5px 15px;
    background: none;
    color: #1a3b5d;

    &:hover,
    &:focus {
      border-color: #38a294;
    }

    &:focus {
      box-shadow: 0px 10px 20px -13px rgba(32, 56, 117, 0.35);
    }
    &.-select {
      -webkit-appearance: none;
      background-image: url("/img/select.png");
      background-size: 12px;
      background-position: 90% center;
      background-repeat: no-repeat;
      padding-right: 30px;
    }
  }
}
</style>

Here we created a form with all the fields, setup state and added some styling. You might be wondering why some inputs are using v-model and why some are using :value. This is because for the cvv and cardnumber we will use vue-mask to set the value for us, more on that later.

For the month dropdown, we iterate from 1-12 and format the number with 2 digits. On the year dropdown, we start iterating from the current year and stop when we have iterated 12 times.

Next step is to import it in our App.vue component and add some general styling. In components/App.vue add the following code.

<template>
  <div id="app">
    <credit-card-form />
  </div>
</template>

<script>
import CreditCardForm from "./components/CreditCardForm";

export default {
  name: "app",
  components: {
    CreditCardForm
  }
};
</script>

<style lang="scss">
#app {
  @import url("https://fonts.googleapis.com/css?family=Source+Code+Pro:400,700&display=swap");
  @import url("https://fonts.googleapis.com/css?family=Roboto&display=swap");
  font-family: "Roboto", sans-serif;
  margin: 0 auto;
  text-align: center;
  color: #2c3e50;

  * {
    box-sizing: border-box;
    &:focus {
      outline: none;
    }
  }
}
</style>

Run npm serve in the terminal.

If you open up http://localhost:8080 you should now be able to see the form.

Credit card form

Creating our Credit Card component

This component will work as a wrapper for frontside and backside of our credit card. In components create CreditCard.vue and add the following code.

<template>
  <div class="credit-card">
    <div
      class="credit-card__inner"
      :class="{
        'show-back': showBack
      }"
    ></div>
  </div>
</template>

<script>
export default {
 data() {
    return {
      backgroundImage: this.randomCard()
    };
  },
  methods: {
    randomCard() {
      return `/img/card-${Math.floor(Math.random() * 5) + 1}.jpg`;
    }
  },
  props: {
    cardNumber: String,
    expireMonth: String,
    expireYear: String,
    cvv: String,
    name: String,
    showBack: Boolean,
    symbolImage: String
  }
};
</script>

<style scoped lang="scss">
.credit-card {
  font-family: "Source Code Pro", monospace;
  max-width: 420px;
  width: 100%;
  height: 245px;
  background-color: transparent;
  color: white;
  perspective: 1000px;
  display: inline-block;

  &:hover &__inner {
    transform: rotateY(180deg);
  }

  &__inner {
    position: relative;
    width: 100%;
    height: 100%;
    text-align: center;
    transition: transform 0.6s;
    transform-style: preserve-3d;
  }

  @media screen and (max-width: 480px) {
    height: 210px;
  }

  @media screen and (max-width: 360px) {
    height: 180px;
  }
}
.show-back {
  transform: rotateY(180deg);
  &:hover {
    transform: rotateY(0deg);
  }
}
</style>

We will supply this component with all the form values as props plus two additional props, showBack and symbolmage.

showBack Will control what side is showing if true show back else show front.

symbolImage Is the path to provider symbol, for example Mastercard or Visa.

randomCard This method will give us back a random image as background, if you open up public/img you will see 5 images with numbers, img1 to img5.

Feel free to replace these images with any images you want. I recommend Unsplash they provide great quality images that can be used in your projects. If you decide to add or remove images just remember to update the number 5 in randomCard method to correct number.

You might be wondering what the transform properties are used for they will add a nice flipping effect.

In componenets/CreditCardForm.vue add the following code below card-form__inner

<div class="card-container">
  <credit-card
    :expireYear="expireYear"
    :expireMonth="expireMonth"
    :cardNumber="cardNumber"
    :name="name"
    :cvv="cvv"
    :showBack="showBack"
    :symbolImage="'/img/' + symbolImage + '.png'"
  />
</div>

Import CreditCard.vue create a new components object with CreditCard inside. At the end of the data object add two new properties showBack and symbolImage.

<script>
import CreditCard from "./CreditCard.vue";
export default {
  components: {
    CreditCard
  },
  data() {
    return {
      ...
      showBack: false,
      symbolImage: "mastercard"
    };
  },
  ...
}
</script>

Creating frontside of the credit card

In components create CardFront.vue and add the following code.

<template>
  <div class="card-front">
    <img class="card-front__image" :src="backgroundImage" />
    <img class="card-front__symbol" :src="symbolImage" />
    <img class="card-front__chip" :src="'/img/chip.png'" />
    <p class="card-front__number">{{ cardNumber }}</p>
    <div class="card-front__info">
      <p>Expires</p>
      <p class="card-front__expires value">
        {{ expireMonth || "MM" }} /
        {{ (expireYear && sliceYear) || "YY" }}
      </p>
    </div>
    <div class="card-front__info left">
      <p>Card Owner</p>
      <p class="value">{{ name || "name" }}</p>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    backgroundImage: String,
    symbolImage: String,
    cardNumber: String,
    expireMonth: String,
    expireYear: String,
    name: String
  },
  computed: {
    sliceYear() {
      return this.expireYear.toString().slice(2);
    }
  }
};
</script>

<style scoped lang="scss">
$x-space: 24px;
$y-space: 16px;
.card-front {
  position: absolute;
  width: 100%;
  height: 100%;
  backface-visibility: hidden;
  z-index: 100;
  &__image {
    width: 100%;
    height: 100%;
    border-radius: 16px;
  }
  &__number {
    position: absolute;
    font-size: 26px;
    top: 35%;
    left: x-space;
  }
  &__chip {
    position: absolute;
    top: $y-space;
    left: $x-space;
    height: 44px;
  }
  &__symbol {
    position: absolute;
    top: $y-space;
    right: $x-space;
    height: 48px;
  }
  &__info {
    position: absolute;
    bottom: $y-space;
    right: $x-space;
    color: white;
    text-align: left;
    margin: 0;
    &.left {
      left: $x-space;
    }
    .value {
      font-weight: bold;
    }
    p {
      margin: 0;
    }
  }
  &__expires {
    right: auto;
    left: $x-space;
  }
  @media screen and (max-width: 480px) {
    &__number {
      font-size: 22px;
    }
    &__info {
      font-size: 12px;
    }
    &__chip {
      height: 34px;
    }
    &__symbol {
      height: 38px;
    }
  }
  @media screen and (max-width: 360px) {
    &__number {
      font-size: 18px;
    }
  }
}
</style>

Here we create the structure for the frontside of our card. This contains the typcial data you would see on a credit card. We also specify some default values in case no values are provided for example

{{ expireMonth || "MM" }} /
{{ (expireYear && sliceYear) || "YY" }}

Most of the css is to adjust positon of different elements.

sliceYear is used to format year from 4 digits to 2, for example 2019 -> 19.

Let's import it in components/CreditCard.vue and see how it looks.

Inside the div with the class card-inner add

<card-front
  :backgroundImage="backgroundImage"
  :symbolImage="symbolImage"
  :cardNumber="cardNumber"
  :expireMonth="expireMonth"
  :expireYear="expireYear"
  :name="name"
/>

Import CardFront.vue and create components object with CardFront as key.

<script>
import CardFront from "./CardFront";
export default {
  components: {
    CardFront
  },
}
...
</script>

Open up http://localhost:8080 and you should be able to see the front of our credit card. If you update the card owner in the form it will also be updated in the card, however, you might notice nothing happens if you update the card number we will fix this soon.

Try to reload the page multiple times and you should see how the background for the card is changing. If you hover over the card you will see it's flipping, however, it looks a bit strange because we have not yet created the backside of the card.

Credit card front

Creating backside of the credit card

In components create CardBack.vue and add the following code.

<template>
 <div class="card-back">
    <img class="card-back__image" :src="backgroundImage" />
    <div class="card-back__stripe"></div>
    <div class="card-back__cvv">
      {{ cvv }}
    </div>
  </div>
</template>

<script>
export default {
  props: {
    cvv: String,
    backgroundImage: String
  }
};
</script>

<style scoped lang="scss">
.card-back {
  position: absolute;
  width: 100%;
  height: 100%;
  backface-visibility: hidden;
  z-index: 100;
  transform: rotateY(180deg);

  &__image {
    width: 100%;
    height: 100%;
    border-radius: 16px;
  }

  &__stripe {
    padding: 24px;
    position: relative;
    bottom: 90%;
    opacity: 0.8;
    width: 100%;
    background-color: black;
  }

  &__cvv {
    border-radius: 4px;
    position: absolute;
    bottom: 35%;
    left: 50%;
    transform: translateX(-50%);
    width: 92%;
    background-color: white;
    color: black;
    text-align: right;
    font-size: 22px;
    min-height: 45px;
    padding: 8px;
  }

  @media screen and (max-width: 360px) {
    &__stripe {
      padding: 18px;
    }
    &__cvv {
      min-height: 36px;
      padding: 4px;
    }
  }
}
</style>

This component is quite small and most of it again is css. We give backgroundImage same as our CardFront.vue, display magnetic stripe and cvv.

Let's import it in CreditCard.vue and see how it looks.

Below <card-front /> add this code

 <card-back :cvv="cvv" :backgroundImage="backgroundImage" />

Import CardBack and add it to components.

<script>
import CardFront from "./CardFront";
import CardBack from "./CardBack";
export default {
  components: {
    CardFront,
    CardBack
  },
}
...
</script>

f you now open up the web applications and hover the card you should see it flips and show the backside.

Credit card back

Fixing card number and cvv, detect the type of card

As you may have noticed cvv or card number is not working. It's now time to fix it and use vue-imask to restrict a user from entering wrong formats.

In src directory create a file called masks.js with the following content.

export const cardMasks = {
  mask: [
    {
      mask: "0000 0000 0000 0000",
      regex: "^(5[1-5]\\d{0,2}|22[2-9]\\d{0,1}|2[3-7]\\d{0,2})\\d{0,12}",
      cardtype: "mastercard"
    },
    {
      mask: "0000 0000 0000 0000",
      regex: "^4\\d{0,15}",
      cardtype: "visa"
    },
    {
      mask: "0000 0000 0000 0000",
      cardtype: "Unknown"
    }
  ],

  dispatch(appended, dynamicMasked) {
    const number = (dynamicMasked.value + appended).replace(/\D/g, "");

    for (let i = 0; i < dynamicMasked.compiledMasks.length; i++) {
      const re = new RegExp(dynamicMasked.compiledMasks[i].regex);
      if (number.match(re) != null) {
        return dynamicMasked.compiledMasks[i];
      }
    }
  }
};

export const cvvMask = {
  mask: "0000"
};

Here I added 3 masks visa, mastercard and unkown in case it does not match. If you would like to support more types of cards, for example, Amex Card just do a quick google search for credit card regex. If you do, don't forget to add matching icon in public/img

what does dispatch do? It might look complicated but basically, all that it does is to check if the pattern matches any mask, in that case return it. More information about dispatch can be found at the documentation imask.js.

Open up CreditCardForm.vue and import IMaskDirective and { cardMasks, cvvMask }, also at the end of the export default object add IMaskDirective as a directive.

<script>
import CreditCard from "./CreditCard.vue";
import { IMaskDirective } from "vue-imask";
import { cardMasks, cvvMask } from "@/masks";
export default {
  ...
  directives: {
    imask: IMaskDirective
  }
}
</script>

under submitCard method all the following methods and update data properties to include cardMasks and cvvMask.

<script>
...
export default {
  ...
  data() {
    return {
      cardMasks: cardMasks,
      cvvMask: cvvMask,
      ...
    };
  },
  methods: {
    onAcceptCardType(e) {
      const maskRef = e.detail;
      const type = maskRef.masked.currentMask.cardtype;

      if (type !== "Unknown") {
        this.symbolImage = type;
      }

      this.cardNumber = maskRef.value;
    },
    onAcceptCvv(e) {
      const maskRef = e.detail;
      this.cvv = maskRef.value;
    }
  },
  ...
};
</script>

Here we get the reference of the element, we then check if the type is not unknown and update symbolImage. vue-mask is adding those properties and remember in mask.js we have 3 types Unknown, mastercard and visa. After that, we update this.cardNumber to get the value from our reference element.

onAcceptCvv get the reference and updates the value.

Now Update cardNumber input with the following code

<input
  :value="cardNumber"
  autofocus
  id="cardNumber"
  class="card-input__input"
  autocomplete="off"
  v-imask="cardMasks"
  @accept="onAcceptCardType"
/>

and update cardCvv input

 <input
  class="card-input__input"
  id="cardCvv"
  :value="cvv"
  autocomplete="off"
  v-imask="cvvMask"
  @accept="onAcceptCvv"
  />

If you now edit the input values for number and cvv you should see how it also updates the card. It will also format it right by adding spaces and limiting length.

Try to add 5372071303778443 as card number and change it to 4197394215553472, you will notice it will now detect automatically if it's a visa or mastercard.

Turn card on focus input

When we are focusing cvv input we want to show the backside of our card else show frontside. All we have to do is adding @focus and @blur to the cvv input in CreditCardForm.vue. This will set showBack to true or false, we then pass it as props.

 <input
  class="card-input__input"
  id="cardCvv"
  :value="cvv"
  autocomplete="off"
  v-imask="cvvMask"
  @accept="onAcceptCvv"
  @focus="showBack = true"
  @blur="showBack = false"
  />

Summary

You learned how to create a credit card form in vue.js, hopefully, you were able to follow along. If you ever got stuck full source code can be found at https://github.com/patni1992/vue-credit-card-form.git.

Feel free to edit the code how you like, some ideas might be to add more masks or change background images.

I hope you enjoyed this tutorial.