Skip to content

Card Element ​

The Card Element provides card-specific payment collection. Use it when you only need to accept card payments or want maximum control over the card input UI.

Choosing Between Card Elements ​

ComponentUse Case
VueStripeCardElementSingle input for card number, expiry, and CVC
Split elementsSeparate inputs for custom layouts

Single Card Element ​

The simplest way to collect card details:

vue
<script setup>
import { ref } from 'vue'
import type { Stripe, StripeElements } from '@stripe/stripe-js'
import {
  VueStripeProvider,
  VueStripeElements,
  VueStripeCardElement
} from '@vue-stripe/vue-stripe'

const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const clientSecret = ref('pi_..._secret_...')

// Capture instances from component events
const stripeInstance = ref<Stripe | null>(null)
const elementsInstance = ref<StripeElements | null>(null)

const onStripeLoad = (stripe: Stripe) => {
  stripeInstance.value = stripe
}

const onElementsReady = (elements: StripeElements) => {
  elementsInstance.value = elements
}

const handleSubmit = async () => {
  if (!stripeInstance.value || !elementsInstance.value) return

  const cardElement = elementsInstance.value.getElement('card')

  const { error, paymentIntent } = await stripeInstance.value.confirmCardPayment(
    clientSecret.value,
    {
      payment_method: {
        card: cardElement!
      }
    }
  )

  if (error) {
    console.error(error.message)
  } else if (paymentIntent?.status === 'succeeded') {
    console.log('Payment succeeded!')
  }
}
</script>

<template>
  <VueStripeProvider :publishable-key="publishableKey" @load="onStripeLoad">
    <VueStripeElements @ready="onElementsReady">
      <form @submit.prevent="handleSubmit">
        <VueStripeCardElement />
        <button type="submit">Pay</button>
      </form>
    </VueStripeElements>
  </VueStripeProvider>
</template>

No clientSecret required

Unlike Payment Element, Card Element can work without a clientSecret on StripeElements. You pass the secret directly to confirmCardPayment.

Split Card Elements ​

For custom layouts, use separate elements for each field:

vue
<script setup>
import {
  VueStripeProvider,
  VueStripeElements,
  VueStripeCardNumberElement,
  VueStripeCardExpiryElement,
  VueStripeCardCvcElement
} from '@vue-stripe/vue-stripe'

const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
</script>

<template>
  <VueStripeProvider :publishable-key="publishableKey">
    <VueStripeElements>
      <div class="card-form">
        <div class="field">
          <label>Card number</label>
          <VueStripeCardNumberElement />
        </div>
        <div class="row">
          <div class="field">
            <label>Expiry</label>
            <VueStripeCardExpiryElement />
          </div>
          <div class="field">
            <label>CVC</label>
            <VueStripeCardCvcElement />
          </div>
        </div>
      </div>
    </VueStripeElements>
  </VueStripeProvider>
</template>

Confirming with Split Elements ​

When using split elements, reference the card number element. Use the @load and @ready events to capture the Stripe and Elements instances:

js
// Capture instances from component events
const stripeInstance = ref(null)
const elementsInstance = ref(null)

const onStripeLoad = (stripe) => {
  stripeInstance.value = stripe
}

const onElementsReady = (elements) => {
  elementsInstance.value = elements
}

const handleSubmit = async () => {
  if (!stripeInstance.value || !elementsInstance.value) return

  // Get the card number element (expiry and CVC are linked automatically)
  const cardNumberElement = elementsInstance.value.getElement('cardNumber')

  const { error, paymentIntent } = await stripeInstance.value.confirmCardPayment(
    clientSecret.value,
    {
      payment_method: {
        card: cardNumberElement
      }
    }
  )
}

Complete Example ​

vue
<script setup lang="ts">
import { ref } from 'vue'
import type { Stripe, StripeElements } from '@stripe/stripe-js'
import {
  VueStripeProvider,
  VueStripeElements,
  VueStripeCardElement
} from '@vue-stripe/vue-stripe'

const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const clientSecret = 'pi_..._secret_...' // From your backend

const processing = ref(false)
const cardComplete = ref(false)
const errorMessage = ref('')
const success = ref(false)

// Capture instances from component events
const stripeInstance = ref<Stripe | null>(null)
const elementsInstance = ref<StripeElements | null>(null)

const onStripeLoad = (stripe: Stripe) => {
  stripeInstance.value = stripe
}

const onElementsReady = (elements: StripeElements) => {
  elementsInstance.value = elements
}

const handleChange = (event: { complete: boolean; error?: { message: string } }) => {
  cardComplete.value = event.complete
  errorMessage.value = event.error?.message || ''
}

const handleSubmit = async () => {
  if (!stripeInstance.value || !elementsInstance.value) return

  processing.value = true
  errorMessage.value = ''

  const cardElement = elementsInstance.value.getElement('card')

  const { error, paymentIntent } = await stripeInstance.value.confirmCardPayment(
    clientSecret,
    {
      payment_method: {
        card: cardElement!,
        billing_details: {
          name: 'Customer Name'
        }
      }
    }
  )

  if (error) {
    errorMessage.value = error.message || 'Payment failed'
    processing.value = false
  } else if (paymentIntent?.status === 'succeeded') {
    success.value = true
  }
}

const cardStyles = {
  base: {
    fontSize: '16px',
    color: '#424770',
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
    '::placeholder': {
      color: '#aab7c4'
    }
  },
  invalid: {
    color: '#9e2146',
    iconColor: '#9e2146'
  }
}
</script>

<template>
  <div class="card-form">
    <div v-if="success" class="success">
      Payment successful!
    </div>

    <VueStripeProvider v-else :publishable-key="publishableKey" @load="onStripeLoad">
      <VueStripeElements @ready="onElementsReady">
        <form @submit.prevent="handleSubmit">
          <div class="card-field">
            <label>Card details</label>
            <VueStripeCardElement
              :options="{ style: cardStyles, hidePostalCode: true }"
              @change="handleChange"
            />
          </div>

          <div v-if="errorMessage" class="error">
            {{ errorMessage }}
          </div>

          <button
            type="submit"
            :disabled="processing || !cardComplete"
          >
            {{ processing ? 'Processing...' : 'Pay' }}
          </button>
        </form>
      </VueStripeElements>
    </VueStripeProvider>
  </div>
</template>

<style scoped>
.card-form {
  max-width: 400px;
  margin: 0 auto;
}

.card-field {
  margin-bottom: 16px;
}

.card-field label {
  display: block;
  margin-bottom: 8px;
  font-size: 14px;
  font-weight: 500;
}

:deep(.vue-stripe-card-element) {
  padding: 12px;
  border: 1px solid #e6e6e6;
  border-radius: 4px;
  background: white;
}

:deep(.vue-stripe-card-element:focus-within) {
  border-color: #5469d4;
  box-shadow: 0 0 0 1px #5469d4;
}

button {
  width: 100%;
  background: #5469d4;
  color: white;
  padding: 12px;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.error {
  color: #9e2146;
  font-size: 14px;
  margin-bottom: 16px;
}

.success {
  color: #00d66b;
  padding: 20px;
  text-align: center;
}
</style>

Card Element vs Payment Element ​

FeatureCard ElementPayment Element
Payment methodsCards only40+ methods
Layout controlHighMedium
Setup complexityLowerHigher
Automatic updatesNoYes
clientSecret requirementOptionalRequired

When to Use Card Element ​

  • You only accept card payments
  • You need precise control over field layout
  • You want to match an existing design system
  • You're building a POS-style interface

Next Steps ​

Released under the MIT License.