Skip to content

Style elements ​

Make Stripe's payment fields match your design. Two layers are involved:

  • Inside the iframes (inputs, labels, tabs): Stripe draws these. Style them with Stripe's Appearance API, passed through Vue Stripe props.
  • Around the iframes (the wrapper, loading text, error box): Vue Stripe renders these. Style them with CSS classes, or replace them with slots.

Your page CSS cannot reach inside Stripe's iframes, so use the Appearance API for everything there.

1. Import the stylesheet ​

The default styles for the wrapper markup ship in a separate file. Import it once in your app entry:

ts
// main.ts
import '@vue-stripe/vue-stripe/style.css'

Leave it out only if you style every class listed in step 4 yourself.

2. Theme the fields with the Appearance API ​

Pass appearance to VueStripeElements. It applies to every element inside: Payment, Express Checkout, Address, Link Authentication and the other Payment Element family components.

vue
<script setup lang="ts">
import { VueStripeElements, VueStripePaymentElement } from '@vue-stripe/vue-stripe'
import type { Appearance } from '@vue-stripe/vue-stripe'

defineProps<{ clientSecret: string }>()

const appearance = {
  theme: 'stripe', // 'stripe', 'night' or 'flat'
  variables: {
    colorPrimary: '#635bff',
    colorBackground: '#ffffff',
    colorText: '#30313d',
    colorDanger: '#df1b41',
    fontFamily: 'Inter, system-ui, sans-serif',
    fontSizeBase: '15px',
    spacingUnit: '4px',
    borderRadius: '8px'
  },
  rules: {
    '.Input': { border: '1px solid #d1d5db', boxShadow: 'none' },
    '.Input:focus': { borderColor: '#635bff', boxShadow: '0 0 0 3px rgba(99, 91, 255, 0.15)' },
    '.Input--invalid': { borderColor: '#df1b41' },
    '.Label': { fontWeight: '500' }
  }
} satisfies Appearance

const fonts = [{ cssSrc: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500' }]
</script>

<template>
  <VueStripeElements :client-secret="clientSecret" :appearance="appearance" :fonts="fonts">
    <VueStripePaymentElement />
  </VueStripeElements>
</template>
  • theme picks a base look; variables adjust colors, fonts, spacing and radius across all fields; rules target single parts (.Input, .Label, .Tab, .Error, ...) with CSS-like declarations. Stripe lists every variable and rule selector.
  • A font used in fontFamily must be loaded into the iframe with fonts; your page's fonts are not available there.
  • In TypeScript, the appearance prop is typed as Stripe's Appearance, so either annotate the object (const appearance: Appearance = { ... }) or check it with satisfies Appearance, as above. satisfies keeps the literal types (theme: 'night'), which the computed value in the next sample relies on.

Switch themes at runtime ​

Changes to appearance (and locale) are applied to the mounted elements in place, so what the customer typed stays. A dark mode toggle is a computed value:

vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { VueStripeElements, VueStripePaymentElement } from '@vue-stripe/vue-stripe'
import type { Appearance } from '@vue-stripe/vue-stripe'

defineProps<{ clientSecret: string }>()

const dark = ref(false)
const appearance = computed(
  () =>
    ({
      theme: dark.value ? 'night' : 'stripe',
      variables: { colorPrimary: '#7c3aed' }
    }) satisfies Appearance
)
</script>

<template>
  <label><input v-model="dark" type="checkbox" /> Dark mode</label>
  <VueStripeElements :client-secret="clientSecret" :appearance="appearance">
    <VueStripePaymentElement />
  </VueStripeElements>
</template>

VueStripeElements emits @update-end once Stripe has applied the change.

3. Style the Card, IBAN and BECS elements ​

The Card Element, the split card elements (VueStripeCardNumberElement, VueStripeCardExpiryElement, VueStripeCardCvcElement), VueStripeIbanElement and VueStripeAuBankAccountElement predate the Appearance API and ignore it. Style their text with the style option of each element. base applies always; complete, empty and invalid override it in those states:

vue
<script setup lang="ts">
import { VueStripeCardElement } from '@vue-stripe/vue-stripe'

const style = {
  base: {
    color: '#30313d',
    fontFamily: 'Inter, system-ui, sans-serif',
    fontSize: '16px',
    '::placeholder': { color: '#9ca3af' }
  },
  invalid: { color: '#df1b41', iconColor: '#df1b41' }
}
</script>

<template>
  <VueStripeCardElement :options="{ style, hidePostalCode: true }" />
</template>

Pass the same style object to the IBAN and BECS elements. A font in fontFamily must be loaded into the iframe with the fonts prop of VueStripeElements, as in step 2.

Each of these elements has a few more display options:

ElementOptionValues
Card, IBAN, BECSiconStyle'default' or 'solid' (the brand or bank icon)
Card, IBAN, BECShideIcontrue hides the icon
Card, IBAN, BECSdisabledtrue makes the field read-only
CardhidePostalCodetrue drops the postal code field (collect the address elsewhere)
IBANsupportedCountries['SEPA'], required
IBANplaceholderCountryA country code; the placeholder shows that country's IBAN format, for example DE00 0000 0000 0000 0000 00 for 'DE' and NL00 AAAA 0000 0000 00 for 'NL'
vue
<VueStripeIbanElement
  :options="{ supportedCountries: ['SEPA'], placeholderCountry: 'DE', style }"
/>

<VueStripeAuBankAccountElement :options="{ style, iconStyle: 'solid' }" />

Stripe also adds state classes to the node it mounts into (Vue Stripe's -mount div): StripeElement--focus, StripeElement--invalid, StripeElement--complete and StripeElement--empty. Use them for borders that follow the field state:

css
.vue-stripe-card-element-mount.StripeElement--invalid {
  border-color: #df1b41;
}

The classes option renames them if they clash with your CSS. The box itself (border, padding, focus ring) is Vue Stripe's wrapper markup, covered next.

4. Style the wrapper markup ​

Every element component renders the same markup around Stripe's iframe:

html
<div class="vue-stripe-payment-element vue-stripe-element">
  <!-- only when there is an error -->
  <div class="vue-stripe-payment-element-error vue-stripe-element-error">
    <div class="vue-stripe-error-message">The error message</div>
  </div>
  <!-- Stripe mounts its iframe here; while loading it also has
       vue-stripe-payment-element-loading and vue-stripe-element-loading -->
  <div class="vue-stripe-payment-element-mount vue-stripe-element-mount"></div>
  <!-- only while loading -->
  <div class="vue-stripe-payment-element-loader vue-stripe-element-loader">
    <div class="vue-stripe-loading-message">Loading payment form...</div>
  </div>
</div>

The text sits in its own inner div: vue-stripe-error-message and vue-stripe-loading-message, which the stylesheet styles separately from the boxes around them. A #error or #loading slot replaces that inner div with your content.

Each part has a shared class (vue-stripe-element, vue-stripe-element-mount, -error, -loader, -loading) and an element-specific one. The element-specific classes do not all follow one pattern: some use Stripe's camelCase element type and the Link Authentication Element has a short name. They keep the names v5 used, so existing CSS still applies. The wrapper class of each component, which the -mount, -error, -loader and -loading suffixes are added to:

ComponentWrapper class
VueStripePaymentElementvue-stripe-payment-element
VueStripeExpressCheckoutElementvue-stripe-express-checkout-element
VueStripeAddressElementvue-stripe-address-element
VueStripeLinkAuthenticationElementvue-stripe-link-auth
VueStripeContactDetailsElementvue-stripe-contact-details-element
VueStripeTaxIdElementvue-stripe-tax-id-element
VueStripeCurrencySelectorElementvue-stripe-currency-selector-element
VueStripeLinkSignupElementvue-stripe-link-signup-element
VueStripeTermsElementvue-stripe-terms-element
VueStripePaymentMethodMessagingElementvue-stripe-paymentMethodMessaging-element
VueStripeCheckoutFormvue-stripe-checkout-form
VueStripeCardElementvue-stripe-card-element
VueStripeCardNumberElementvue-stripe-cardNumber-element
VueStripeCardExpiryElementvue-stripe-cardExpiry-element
VueStripeCardCvcElementvue-stripe-cardCvc-element
VueStripeIbanElementvue-stripe-iban-element
VueStripeAuBankAccountElementvue-stripe-au-bank-account-element
VueStripeIssuingCardNumberDisplayElementvue-stripe-issuing-card-number-display-element
VueStripeIssuingCardExpiryDisplayElementvue-stripe-issuing-card-expiry-display-element
VueStripeIssuingCardCvcDisplayElementvue-stripe-issuing-card-cvc-display-element
VueStripeIssuingCardPinDisplayElementvue-stripe-issuing-card-pin-display-element
VueStripeIssuingCardCopyButtonElementvue-stripe-issuing-card-copy-button-element
VueStripeIssuingAddToWalletButtonElementvue-stripe-issuing-add-to-wallet-button-element

So the Card Number Element's iframe is mounted in vue-stripe-cardNumber-element-mount, and the Link Authentication Element's error box is vue-stripe-link-auth-error. Each component's API page also shows its markup under "Styling". The default stylesheet gives the Card, split card, IBAN and BECS boxes box-sizing: border-box with a 44px min-height, so they are 44px tall whether or not your app has a global box-sizing reset.

Target the shared classes to restyle every element, or the specific ones for a single element:

css
/* Every element: a quieter error message */
.vue-stripe-element-error {
  border: 0;
  background: none;
  padding: 0.25rem 0;
}

/* Card Element box, matching the inputs around it */
.vue-stripe-card-element-mount {
  border: 1px solid #d1d5db;
  border-radius: 8px;
  padding: 12px 14px;
}
.vue-stripe-card-element-mount:focus-within {
  border-color: #635bff;
}

/* The Payment Element reserves 300px while loading; reserve less */
.vue-stripe-payment-element-mount {
  min-height: 200px;
}

In a component with <style scoped>, the element's markup belongs to the child component, so reach it with :deep():

vue
<style scoped>
.card-field :deep(.vue-stripe-card-element-mount) {
  border: 1px solid #e6e6e6;
  border-radius: 4px;
  padding: 12px;
  background: white;
}
.card-field :deep(.vue-stripe-card-element-mount:focus-within) {
  border-color: #5469d4;
  box-shadow: 0 0 0 1px #5469d4;
}
</style>

Put the border on the -mount part, as the default stylesheet does, not on the outer vue-stripe-card-element; otherwise the field gets two borders.

The providers render vue-stripe-loading / vue-stripe-error (VueStripeProvider) and vue-stripe-elements-loading / vue-stripe-elements-error (VueStripeElements) while they load or fail.

5. Replace the loading and error UI with slots ​

Every element component and provider has #loading and #error slots. Use them to show your own spinner or message instead of the default text:

vue
<VueStripeProvider :publishable-key="publishableKey">
  <template #loading>
    <AppSpinner label="Loading payment form" />
  </template>
  <template #error="{ error }">
    <AppAlert type="error">Payments are unavailable: {{ error }}</AppAlert>
  </template>

  <VueStripeElements :client-secret="clientSecret">
    <VueStripePaymentElement>
      <template #loading>
        <AppSkeleton :lines="4" />
      </template>
      <template #error="{ error }">
        <AppAlert type="error">{{ error }}</AppAlert>
      </template>
    </VueStripePaymentElement>
  </VueStripeElements>
</VueStripeProvider>

An element's #error slot shows load and creation errors. The Card, split card, IBAN and BECS elements also show Stripe's validation message there while the customer types (an incomplete card number, say). The Payment, Address, Link Authentication and Tax ID elements show their validation messages inside the iframe instead, so their #error slot stays empty while the customer types. Errors from confirming a payment are not shown there either; see Handle errors and declines.

Checkout Sessions and Checkout pages ​

IntegrationWhere the look is set
VueStripeCheckoutElementsProvider:elements-options="{ appearance, fonts }", or changeAppearance() from useCheckoutSession() at runtime
VueStripeCheckoutFormProviderThe appearance and fonts props (appearance without rules)
Embedded and hosted CheckoutYour Stripe Dashboard's branding settings; the page cannot be styled from your code

Express Checkout buttons ​

Wallet buttons follow each wallet's brand rules, so only a few things can change: borderRadius from the Appearance API, and the buttonHeight, buttonTheme and buttonType options of VueStripeExpressCheckoutElement:

vue
<VueStripeExpressCheckoutElement
  :options="{ buttonHeight: 48, buttonTheme: { applePay: 'white-outline' }, buttonType: { googlePay: 'buy' } }"
/>

Result ​

Stripe's fields use your colors, fonts and spacing, the surrounding markup matches your form, and loading and error states use your own components.