---
name: payment-method
title: Payment Method
url: https://ui.frontic.com/components/sections/payment-method.md
tier: sections
install: npx @frontic/ui add payment-method
files:
  - components/sections/payment-method/PaymentMethod.vue
  - components/sections/payment-method/index.ts
  - components/sections/payment-method/types.ts
registryDependencies:
  - button
  - card
  - field
  - input-otp
  - payment-card
  - price
  - radio-accordion
dependencies:
  - "@lucide/vue"
---

# Payment Method

Payment card with a radio stack of methods; card number, expiry, security code and 3-D Secure code fields that open for card payment; and a pay button showing the total.

## Install

```sh
npx @frontic/ui add payment-method
```

Built from: [Button](https://ui.frontic.com/components/ui/button.md), [Card](https://ui.frontic.com/components/ui/card.md), [Field](https://ui.frontic.com/components/ui/field.md), [Input OTP](https://ui.frontic.com/components/ui/input-otp.md), [Payment Card](https://ui.frontic.com/components/ui/payment-card.md), [Price](https://ui.frontic.com/components/ui/price.md), [Radio Accordion](https://ui.frontic.com/components/ui/radio-accordion.md)

npm packages: `@lucide/vue`

## Examples

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { PaymentMethod, type PaymentOption } from '@/components/sections/payment-method'

const options: PaymentOption[] = [
  { value: 'card', label: 'Card', detail: 'Visa · Mastercard · Amex' },
  { value: 'paypal', label: 'PayPal', description: 'You confirm the payment on PayPal and come straight back.' },
  { value: 'klarna', label: 'Klarna · Pay later', detail: 'Unavailable', disabled: true },
]

// Frost Escape Parka, two pairs of Mountain Ghost Winter Tactics Gloves and the Forest Whisperer Beanie
const total = { amount: 14900 + 9000 * 2 + 11200, currency: 'EUR' }

const method = ref('card')
const cardNumber = ref('4921882010434826')
const expiry = ref('08/29')
const cvc = ref('')
const code = ref('5830')
</script>

<template>
  <div class="mx-auto w-full max-w-md">
    <PaymentMethod
      v-model:method="method"
      v-model:card-number="cardNumber"
      v-model:expiry="expiry"
      v-model:cvc="cvc"
      v-model:code="code"
      :options="options"
      :card-brands="['visa', 'mastercard', 'amex']"
      :total="total"
      description="Encrypted and verified by your bank."
      code-hint="Texted by your bank to +49 ••• 77."
    />
  </div>
</template>
```

## API reference

### PaymentMethod

Lets the shopper choose a payment method, enter card details and pay.

Styling: [`meta`](https://ui.frontic.com/DESIGN.md#meta). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `options` (required) | `PaymentOption[]` |  | The payment methods the shopper chooses from, one radio row each. |
| `total` (required) | `PriceType` |  | The amount the pay button charges. |
| `title` | `string` | `'Payment'` | The card's heading. |
| `description` | `string` |  | Shown under the heading. |
| `cardValue` | `string` | `'card'` | The option that reveals the card fields while selected. |
| `cardBrands` | `PaymentCardBrand[]` |  | The card brands whose marks show in the number field, such as the ones the shop accepts. Every brand with a mark when left out. |
| `cardNumberLabel` | `string` | `'Card number'` | Shown above the card number field. |
| `expiryLabel` | `string` | `'Expiry'` | Shown above the expiry field. |
| `cvcLabel` | `string` | `'Security code'` | Shown above the security code field. |
| `codeLabel` | `string` | `'3-D Secure code'` | Shown above the 3-D Secure code field. |
| `codeHint` | `string` |  | Shown under the code field, e.g. where the code was sent. |
| `codeLength` | `number` | `6` | How many digits the code has, one box each. |
| `payLabel` | `string` | `'Pay'` | Shown on the pay button, before the total. |
| `locale` | `string` |  | BCP 47 locale the total is formatted in, e.g. `de-DE`; the ConfigProvider's when left out. |
| `surface` | `'raised' \| 'bare'` | `'raised'` | Bare drops the edge, shadow and inset, for a section placed inside another Card. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `method` | `string` | `''` | The chosen payment method, by its `value`. |
| `cardNumber` | `string` | `''` | The card number's digits. |
| `expiry` | `string` | `''` | The card's expiry as MM/YY. |
| `cvc` | `string` | `''` | The card's security code. |
| `code` | `string` | `''` | The 3-D Secure code typed into the code boxes. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:method` | `value: string` | Fires with the new value when `method` changes; `v-model:method` binds it. |
| `update:cardNumber` | `value: string` | Fires with the new value when `cardNumber` changes; `v-model:cardNumber` binds it. |
| `update:expiry` | `value: string` | Fires with the new value when `expiry` changes; `v-model:expiry` binds it. |
| `update:cvc` | `value: string` | Fires with the new value when `cvc` changes; `v-model:cvc` binds it. |
| `update:code` | `value: string` | Fires with the new value when `code` changes; `v-model:code` binds it. |
| `pay` | `method: string` | Fires with the selected payment method when the shopper clicks the pay button. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-surface` | `'raised' \| 'bare'` | Carries the `surface` prop. |

## Exports

```ts
import { type PaymentOption } from '@/components/sections/payment-method'
```

- `PaymentOption`: below

```ts
interface PaymentOption {
  value: string
  label: string
  /** Shown at the row's end, e.g. the accepted cards or why it is unavailable. */
  detail?: string
  /** Shown below the row while it is selected. */
  description?: string
  disabled?: boolean
}
```

## Styling

| Part | Recipe |
| --- | --- |
| `meta` | [`meta`](https://ui.frontic.com/DESIGN.md#meta) |

## Source

`components/sections/payment-method/PaymentMethod.vue`

```vue
<script lang="ts">
/** Lets the shopper choose a payment method, enter card details and pay. */
export default {}
</script>

<script setup lang="ts">
import { LockIcon } from '@lucide/vue'

import type { HTMLAttributes } from 'vue'
import type { PaymentCardBrand } from '@library/components/ui/payment-card'
import type { PriceType } from '@library/components/ui/price'
import type { PaymentOption } from './types'
import { Button } from '@library/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@library/components/ui/card'
import { Field, FieldDescription, FieldGroup, FieldLabel } from '@library/components/ui/field'
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@library/components/ui/input-otp'
import { PaymentCard, PaymentCardCvc, PaymentCardExpiry, PaymentCardNumber } from '@library/components/ui/payment-card'
import { Price } from '@library/components/ui/price'
import { RadioAccordion, RadioAccordionContent, RadioAccordionItem, RadioAccordionTrigger } from '@library/components/ui/radio-accordion'

const props = withDefaults(
  defineProps<{
    /** The payment methods the shopper chooses from, one radio row each. */
    options: PaymentOption[]
    /** The amount the pay button charges. */
    total: PriceType
    /** The card's heading. */
    title?: string
    /** Shown under the heading. */
    description?: string
    /** The option that reveals the card fields while selected. */
    cardValue?: string
    /** The card brands whose marks show in the number field, such as the ones the shop accepts. Every brand with a mark when left out. */
    cardBrands?: PaymentCardBrand[]
    /** Shown above the card number field. */
    cardNumberLabel?: string
    /** Shown above the expiry field. */
    expiryLabel?: string
    /** Shown above the security code field. */
    cvcLabel?: string
    /** Shown above the 3-D Secure code field. */
    codeLabel?: string
    /** Shown under the code field, e.g. where the code was sent. */
    codeHint?: string
    /** How many digits the code has, one box each. */
    codeLength?: number
    /** Shown on the pay button, before the total. */
    payLabel?: string
    /** BCP 47 locale the total is formatted in, e.g. `de-DE`; the ConfigProvider's when left out. */
    locale?: string
    /** Bare drops the edge, shadow and inset, for a section placed inside another Card. */
    surface?: 'raised' | 'bare'
    class?: HTMLAttributes['class']
  }>(),
  {
    surface: 'raised',
    title: 'Payment',
    cardValue: 'card',
    cardNumberLabel: 'Card number',
    expiryLabel: 'Expiry',
    cvcLabel: 'Security code',
    codeLabel: '3-D Secure code',
    codeLength: 6,
    payLabel: 'Pay',
  },
)
const emit = defineEmits<{
  /** Fires with the selected payment method when the shopper clicks the pay button. */
  pay: [method: string]
}>()
/** The chosen payment method, by its `value`. */
const method = defineModel<string>('method', { default: '' })
/** The card number's digits. */
const cardNumber = defineModel<string>('cardNumber', { default: '' })
/** The card's expiry as MM/YY. */
const expiry = defineModel<string>('expiry', { default: '' })
/** The card's security code. */
const cvc = defineModel<string>('cvc', { default: '' })
/** The 3-D Secure code typed into the code boxes. */
const code = defineModel<string>('code', { default: '' })
</script>

<template>
  <Card :surface="surface" :class="props.class">
    <CardHeader>
      <CardTitle>{{ title }}</CardTitle>
      <CardDescription v-if="description">{{ description }}</CardDescription>
    </CardHeader>
    <CardContent>
      <RadioAccordion v-model="method" variant="outline">
        <RadioAccordionItem v-for="option in options" :key="option.value" :value="option.value" :disabled="option.disabled">
          <RadioAccordionTrigger>
            <span v-if="option.detail" class="flex flex-1 flex-wrap items-baseline justify-between gap-x-2">
              <span>{{ option.label }}</span>
              <span data-slot="meta" class="fui-meta">{{ option.detail }}</span>
            </span>
            <template v-else>{{ option.label }}</template>
          </RadioAccordionTrigger>
          <RadioAccordionContent v-if="option.description || option.value === cardValue" class="flex flex-col gap-4">
            <p v-if="option.description">{{ option.description }}</p>
            <PaymentCard v-if="option.value === cardValue">
              <FieldGroup>
                <Field>
                  <FieldLabel>{{ cardNumberLabel }}</FieldLabel>
                  <PaymentCardNumber v-model="cardNumber" :brands="cardBrands" />
                </Field>
                <div class="grid grid-cols-2 gap-4">
                  <Field>
                    <FieldLabel>{{ expiryLabel }}</FieldLabel>
                    <PaymentCardExpiry v-model="expiry" />
                  </Field>
                  <Field>
                    <FieldLabel>{{ cvcLabel }}</FieldLabel>
                    <PaymentCardCvc v-model="cvc" />
                  </Field>
                </div>
                <Field>
                  <FieldLabel>{{ codeLabel }}</FieldLabel>
                  <InputOTP v-model="code" :maxlength="codeLength" inputmode="numeric">
                    <InputOTPGroup>
                      <InputOTPSlot v-for="index in codeLength" :key="index" :index="index - 1" />
                    </InputOTPGroup>
                  </InputOTP>
                  <FieldDescription v-if="codeHint">{{ codeHint }}</FieldDescription>
                </Field>
              </FieldGroup>
            </PaymentCard>
          </RadioAccordionContent>
        </RadioAccordionItem>
      </RadioAccordion>
    </CardContent>
    <CardFooter>
      <Button color="checkout" class="w-full" @click="emit('pay', method)">
        <LockIcon data-icon />
        {{ payLabel }}
        <Price :price="total" size="sm" :locale="locale" class="text-current" />
      </Button>
    </CardFooter>
  </Card>
</template>
```

`components/sections/payment-method/types.ts`

```ts
export interface PaymentOption {
  value: string
  label: string
  /** Shown at the row's end, e.g. the accepted cards or why it is unavailable. */
  detail?: string
  /** Shown below the row while it is selected. */
  description?: string
  disabled?: boolean
}
```
