Integrate Stripe.js into a Vue.js application by loading the Stripe script, mounting a Payment Element inside a Vue component, and confirming payments with a PaymentIntent client secret from your backend. This guide covers the full flow from form rendering to successful payment confirmation.
Building a Stripe Payment Form in Vue.js
Stripe.js and Elements let you collect card details securely without sensitive data touching your server. In a Vue.js app, you load Stripe.js, create an Elements instance with a client secret from your backend, and mount the Payment Element inside a Vue component. When the user submits the form, you call stripe.confirmPayment to finalize the charge. This tutorial shows you every step.
Prerequisites
- A Vue 3 project created with Vite or Vue CLI
- A backend endpoint that creates a PaymentIntent and returns its client_secret
- Your Stripe publishable key (pk_test_...)
- Basic familiarity with Vue 3 Composition API
Step-by-step guide
Install the Stripe.js loader
Install the Stripe.js loader
Use the official @stripe/stripe-js package to load Stripe.js asynchronously. This avoids adding a script tag manually and gives you TypeScript types.
1npm install @stripe/stripe-jsExpected result: @stripe/stripe-js is added to your package.json dependencies.
Create a Stripe composable
Create a Stripe composable
Create a composable that initializes the Stripe instance once and provides it to your components. Use your publishable key (pk_test_...) — never the secret key.
1// src/composables/useStripe.ts2import { loadStripe } from '@stripe/stripe-js';3import type { Stripe } from '@stripe/stripe-js';4import { ref } from 'vue';56const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);7const stripe = ref<Stripe | null>(null);89export function useStripe() {10 const init = async () => {11 stripe.value = await stripePromise;12 };1314 return { stripe, init };15}Expected result: A reusable composable that provides the Stripe instance throughout your Vue app.
Build the PaymentForm component
Build the PaymentForm component
Create a component that fetches a PaymentIntent client secret from your backend, mounts the Stripe Payment Element, and handles form submission.
1<!-- src/components/PaymentForm.vue -->2<template>3 <form @submit.prevent="handleSubmit">4 <div id="payment-element" />5 <button type="submit" :disabled="loading">6 {{ loading ? 'Processing...' : 'Pay now' }}7 </button>8 <p v-if="errorMessage" class="error">{{ errorMessage }}</p>9 </form>10</template>1112<script setup lang="ts">13import { ref, onMounted } from 'vue';14import { useStripe } from '../composables/useStripe';1516const { stripe, init } = useStripe();17const loading = ref(false);18const errorMessage = ref('');19let elements: any = null;2021onMounted(async () => {22 await init();23 const res = await fetch('/api/create-payment-intent', {24 method: 'POST',25 headers: { 'Content-Type': 'application/json' },26 body: JSON.stringify({ amount: 2000 })27 });28 const { client_secret } = await res.json();2930 elements = stripe.value!.elements({ clientSecret: client_secret });31 const paymentElement = elements.create('payment');32 paymentElement.mount('#payment-element');33});3435async function handleSubmit() {36 if (!stripe.value || !elements) return;37 loading.value = true;38 errorMessage.value = '';3940 const { error } = await stripe.value.confirmPayment({41 elements,42 confirmParams: {43 return_url: window.location.origin + '/payment-success'44 }45 });4647 if (error) {48 errorMessage.value = error.message || 'Payment failed.';49 }50 loading.value = false;51}52</script>Expected result: A payment form renders with card fields. Submitting the form confirms the payment and redirects to /payment-success.
Create the backend PaymentIntent endpoint
Create the backend PaymentIntent endpoint
Your backend creates a PaymentIntent and returns the client_secret. This Node.js Express example uses the Stripe secret key (sk_test_...) which must stay server-side only.
1// server.js (Node.js + Express)2const express = require('express');3const Stripe = require('stripe');4const stripe = Stripe(process.env.STRIPE_SECRET_KEY);5const app = express();67app.use(express.json());89app.post('/api/create-payment-intent', async (req, res) => {10 try {11 const intent = await stripe.paymentIntents.create({12 amount: req.body.amount, // amount in cents13 currency: 'usd',14 automatic_payment_methods: { enabled: true }15 });16 res.json({ client_secret: intent.client_secret });17 } catch (err) {18 res.status(400).json({ error: err.message });19 }20});2122app.listen(3001, () => console.log('Server on port 3001'));Expected result: POST /api/create-payment-intent returns { client_secret: 'pi_..._secret_...' }.
Test the payment flow
Test the payment flow
Run both your Vue dev server and your backend. Use the test card number 4242424242424242 with any future expiry and any CVC to complete a test payment.
1// Test card:2// Number: 4242 4242 4242 42423// Expiry: 12/344// CVC: 123Expected result: The payment succeeds, you are redirected to /payment-success, and the PaymentIntent appears as succeeded in your Stripe test Dashboard.
Complete working example
1<!-- src/components/PaymentForm.vue -->2<template>3 <div class="payment-container">4 <h2>Complete Your Payment</h2>5 <form @submit.prevent="handleSubmit">6 <div id="payment-element" />7 <button type="submit" :disabled="loading || !ready">8 {{ loading ? 'Processing...' : `Pay $${(amount / 100).toFixed(2)}` }}9 </button>10 <p v-if="errorMessage" class="error">{{ errorMessage }}</p>11 </form>12 </div>13</template>1415<script setup lang="ts">16import { ref, onMounted } from 'vue';17import { loadStripe } from '@stripe/stripe-js';18import type { Stripe, StripeElements } from '@stripe/stripe-js';1920const props = defineProps<{ amount: number }>();2122const loading = ref(false);23const ready = ref(false);24const errorMessage = ref('');25let stripe: Stripe | null = null;26let elements: StripeElements | null = null;2728onMounted(async () => {29 stripe = await loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);30 if (!stripe) {31 errorMessage.value = 'Failed to load Stripe.';32 return;33 }3435 const res = await fetch('/api/create-payment-intent', {36 method: 'POST',37 headers: { 'Content-Type': 'application/json' },38 body: JSON.stringify({ amount: props.amount })39 });4041 if (!res.ok) {42 errorMessage.value = 'Could not initialize payment.';43 return;44 }4546 const { client_secret } = await res.json();47 elements = stripe.elements({48 clientSecret: client_secret,49 appearance: { theme: 'stripe' }50 });5152 const paymentElement = elements.create('payment');53 paymentElement.mount('#payment-element');54 paymentElement.on('ready', () => { ready.value = true; });55});5657async function handleSubmit() {58 if (!stripe || !elements) return;59 loading.value = true;60 errorMessage.value = '';6162 const { error } = await stripe.confirmPayment({63 elements,64 confirmParams: {65 return_url: `${window.location.origin}/payment-success`66 }67 });6869 if (error) {70 errorMessage.value = error.message || 'An unexpected error occurred.';71 }72 loading.value = false;73}74</script>7576<style scoped>77.payment-container {78 max-width: 480px;79 margin: 2rem auto;80 padding: 2rem;81}82#payment-element {83 margin-bottom: 1.5rem;84}85button {86 width: 100%;87 padding: 0.75rem;88 background: #635bff;89 color: white;90 border: none;91 border-radius: 6px;92 font-size: 1rem;93 cursor: pointer;94}95button:disabled {96 opacity: 0.6;97 cursor: not-allowed;98}99.error {100 color: #df1b41;101 margin-top: 0.75rem;102}103</style>Common mistakes when using Stripe with Vue.js
Why it's a problem: Using the secret key (sk_test_) in frontend Vue code
How to avoid: Only use the publishable key (pk_test_) on the client. The secret key must remain on your server.
Why it's a problem: Mounting the Payment Element before the client secret is available
How to avoid: Always await the API call that returns the client_secret before calling stripe.elements().
Why it's a problem: Not handling the redirect after confirmPayment
How to avoid: Stripe redirects the user to return_url after confirmation. Make sure that route exists in your Vue Router config.
Why it's a problem: Forgetting to set VITE_STRIPE_PUBLISHABLE_KEY in environment
How to avoid: Create a .env file with VITE_STRIPE_PUBLISHABLE_KEY=pk_test_... and restart the Vite dev server.
Best practices
- Always use the @stripe/stripe-js loader instead of adding a script tag manually
- Keep the Stripe secret key (sk_test_) on your server only — never in Vue components
- Use the Payment Element instead of the Card Element for automatic support of multiple payment methods
- Show a loading state while the Payment Element mounts and during payment confirmation
- Set appearance options on Elements to match your app's design system
- Validate the amount on the server before creating the PaymentIntent
- Handle both the error return from confirmPayment and network failures with try/catch
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Vue 3 app using Vite and a Node.js Express backend. Show me how to create a PaymentForm component that loads Stripe.js, mounts a Payment Element, and confirms a PaymentIntent. Include the backend endpoint that creates the PaymentIntent with automatic_payment_methods enabled.
Build a Vue 3 payment page with a Stripe Payment Element. Use the Composition API, fetch the client_secret from /api/create-payment-intent, mount the element, and handle submit with stripe.confirmPayment. Add loading and error states.
Frequently asked questions
Do I need a backend to use Stripe with Vue.js?
Yes. You need a server-side endpoint to create PaymentIntents using your Stripe secret key. The frontend only handles the UI and payment confirmation.
Can I use Stripe Elements with Vue 2?
Yes, but you will need to use the Options API instead of the Composition API. The Stripe.js integration itself is framework-agnostic.
Why use the Payment Element instead of the Card Element?
The Payment Element automatically supports multiple payment methods like cards, Apple Pay, Google Pay, and bank transfers based on your Stripe Dashboard settings. The Card Element only collects card details.
How do I style the Stripe Payment Element in Vue?
Pass an appearance object when creating the Elements instance. You can set the theme to 'stripe', 'night', or 'flat', and override individual variables like colorPrimary, borderRadius, and fontFamily.
What happens after stripe.confirmPayment is called?
Stripe redirects the user to the return_url you specified. On that page, you can retrieve the payment status from the URL query parameters using stripe.retrievePaymentIntent.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation