Payment Element & Card Element
Recipe
Use PaymentElement for a unified payment input that supports cards, wallets, and bank transfers. Use CardElement when you only need card payments and want finer field-level control. Style both using Stripe's Appearance API.
PaymentElement (recommended for most cases):
// app/checkout/payment-form.tsx
"use client";
import { PaymentElement, useStripe, useElements } from "@stripe/react-stripe-js";
import { useState, type FormEvent } from "react";
export function PaymentForm() {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState<string | null>(null);
const [processing, setProcessing] = useState(false);
async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!stripe || !elements) return;
setProcessing(true);
const { error: submitError } = await elements.submit();
if (submitError) {
setError(submitError.message ?? "Validation error");
setProcessing(false);
return;
}
const { error: confirmError } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: `${window.location.origin}/success` },
});
if (confirmError) {
setError(confirmError.message ?? "Payment failed");
setProcessing(false);
}
}
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
{error && <p className="text-red-500 mt-2">{error}</p>}
<button type="submit" disabled={!stripe || processing}>
{processing ? "Processing..." : "Pay"}
</button>
</form>
);
}CardElement (card-only):
"use client";
import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js";
import { useState, type FormEvent } from "react";
export function CardForm() {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!stripe || !elements) return;
const cardElement = elements.getElement(CardElement);
if (!cardElement) return;
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: "card",
card: cardElement,
});
if (error) {
setError(error.message ?? "Error");
} else {
// Send paymentMethod.id to your server
console.log("PaymentMethod:", paymentMethod.id);
}
}
return (
<form onSubmit={handleSubmit}>
<CardElement options={{ style: { base: { fontSize: "16px" } } }} />
{error && <p className="text-red-500 mt-2">{error}</p>}
<button type="submit" disabled={!stripe}>Pay</button>
</form>
);
}Working Example
A styled payment form that matches a dark-themed app:
// app/checkout/styled-checkout.tsx
"use client";
import { useEffect, useState } from "react";
import { Elements } from "@stripe/react-stripe-js";
import type { Appearance } from "@stripe/stripe-js";
import { stripePromise } from "@/lib/stripe-client";
import { createPaymentIntent } from "@/app/actions/payment";
import { PaymentForm } from "./payment-form";
const appearance: Appearance = {
theme: "night",
variables: {
colorPrimary: "#6366f1",
colorBackground: "#1e1e2e",
colorText: "#e2e8f0",
colorDanger: "#ef4444",
fontFamily: "Inter, system-ui, sans-serif",
borderRadius: "8px",
spacingUnit: "4px",
},
rules: {
".Input": {
border: "1px solid #334155",
boxShadow: "none",
padding: "12px",
},
".Input:focus": {
border: "1px solid #6366f1",
boxShadow: "0 0 0 1px #6366f1",
},
".Label": {
fontWeight: "500",
fontSize: "14px",
},
".Tab": {
border: "1px solid #334155",
borderRadius: "8px",
},
".Tab--selected": {
backgroundColor: "#6366f1",
borderColor: "#6366f1",
},
},
};
export default function StyledCheckout() {
const [clientSecret, setClientSecret] = useState<string | null>(null);
useEffect(() => {
createPaymentIntent(4999).then(({ clientSecret }) => {
setClientSecret(clientSecret);
});
}, []);
if (!clientSecret) return <div className="text-white">Loading...</div>;
return (
<div className="max-w-lg mx-auto p-8 bg-[#1e1e2e] rounded-xl">
<h2 className="text-xl font-bold text-white mb-6">Complete Payment</h2>
<Elements
stripe={stripePromise}
options={{
clientSecret,
appearance,
layout: {
type: "tabs",
defaultCollapsed: false,
},
}}
>
<PaymentForm />
</Elements>
</div>
);
}Deep Dive
How It Works
PaymentElementis a single component that dynamically renders input fields for all enabled payment methods (cards, Apple Pay, Google Pay, bank debits, etc.). Stripe determines which methods to show based on the transaction's currency, amount, and the customer's location.CardElementrenders a single-line card input with number, expiry, and CVC fields combined. It only supports card payments.- The Appearance API controls styling through
theme,variables, andrules. Themes (stripe,night,flat) provide a base; variables override design tokens; rules target specific CSS-like selectors for individual elements. - Layout options for PaymentElement control how payment methods are displayed:
"tabs"shows them as selectable tabs,"accordion"shows them as expandable sections, and"auto"lets Stripe decide. - Billing details collection can be configured per field. By default, PaymentElement collects the fields required by the selected payment method.
Variations
Accordion layout:
<Elements
stripe={stripePromise}
options={{
clientSecret,
layout: { type: "accordion", defaultCollapsed: false, radios: true },
}}
>
<PaymentForm />
</Elements>Control billing details collection:
<PaymentElement
options={{
layout: "tabs",
fields: {
billingDetails: {
name: "auto",
email: "never", // collect email yourself
phone: "never",
address: "auto",
},
},
}}
/>Split card fields (CardNumber, CardExpiry, CardCvc):
import {
CardNumberElement,
CardExpiryElement,
CardCvcElement,
} from "@stripe/react-stripe-js";
function SplitCardForm() {
return (
<div className="space-y-4">
<div>
<label>Card Number</label>
<CardNumberElement />
</div>
<div className="flex gap-4">
<div className="flex-1">
<label>Expiry</label>
<CardExpiryElement />
</div>
<div className="flex-1">
<label>CVC</label>
<CardCvcElement />
</div>
</div>
</div>
);
}TypeScript Notes
- Import
Appearancefrom@stripe/stripe-jsto type your appearance configuration. PaymentElementacceptsPaymentElementPropswhich includes anoptionsprop typed asPaymentElementOptions.CardElementoptions useCardElementOptionswith a different styling API (style.base,style.invalid, etc.) than the Appearance API.
import type { Appearance, LayoutObject } from "@stripe/stripe-js";
const layout: LayoutObject = {
type: "tabs",
defaultCollapsed: false,
};Gotchas
PaymentElementrequires aclientSecreton theElementsprovider. You cannot use it without a PaymentIntent or SetupIntent.CardElementuses a different styling system (styleprop) than PaymentElement (Appearance API). They are not interchangeable.- Do not mount both
PaymentElementandCardElementin the sameElementsprovider. Choose one. - The Appearance API's
rulesuse Stripe's custom selectors (.Input,.Label,.Tab), not standard CSS selectors. Check the docs for the full list. - PaymentElement shows payment methods based on the PaymentIntent's configuration. If you only see cards, check that
automatic_payment_methodsis enabled and your Stripe Dashboard has other methods configured. - Fonts loaded via
@font-facein your CSS are not automatically available inside Stripe Elements. Pass them via thefontsoption onElements.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| PaymentElement | All payment methods, single component, modern | Requires clientSecret, less granular control |
| CardElement | Simple, single-line card input | Cards only, older styling API |
| Split card fields | Full layout control over each field | More markup, cards only |
| Stripe Checkout | Zero UI code | Leaves your site |
FAQs
What is the main difference between PaymentElement and CardElement?
PaymentElementis a unified component that supports cards, wallets, bank transfers, and 40+ payment methods in one componentCardElementis a single-line input that only supports card payments- PaymentElement is recommended for most new integrations
What are the available layout options for PaymentElement?
"tabs"-- payment methods shown as selectable tabs"accordion"-- payment methods shown as expandable sections"auto"-- Stripe decides the best layout
How does the Appearance API work for styling Stripe Elements?
- Choose a base
theme("stripe","night","flat") - Override design tokens via
variables(colors, fonts, border radius, spacing) - Target specific elements via
rulesusing Stripe's custom selectors (.Input,.Label,.Tab)
Gotcha: Can you mount both PaymentElement and CardElement in the same Elements provider?
No. You must choose one or the other within a single <Elements> provider. Mounting both causes conflicts and undefined behavior.
How do you control which billing details PaymentElement collects?
<PaymentElement
options={{
fields: {
billingDetails: {
name: "auto",
email: "never",
phone: "never",
address: "auto",
},
},
}}
/>Set a field to "never" if you collect it yourself in your own form.
What are the split card field components and when should you use them?
CardNumberElement,CardExpiryElement,CardCvcElement- Use them when you need full layout control over individual card fields
- They only support card payments, not other payment methods
Why might PaymentElement only show card payments even with automatic_payment_methods enabled?
Check your Stripe Dashboard payment method settings. automatic_payment_methods shows methods that are both enabled in the Dashboard and applicable to the transaction's currency and customer location.
Gotcha: Why don't custom fonts from your CSS work inside Stripe Elements?
Stripe Elements render inside iframes and cannot access fonts loaded via @font-face in your CSS. You must pass fonts explicitly via the fonts option on the <Elements> component.
TypeScript: How do you type an Appearance configuration object?
import type { Appearance, LayoutObject } from "@stripe/stripe-js";
const appearance: Appearance = {
theme: "night",
variables: { colorPrimary: "#6366f1" },
};
const layout: LayoutObject = { type: "tabs" };What is the difference between CardElement's styling system and the Appearance API?
CardElementuses astyleprop withbase,invalid, andcompletestatesPaymentElementuses the Appearance API withtheme,variables, andrules- The two systems are not interchangeable
TypeScript: What type does the PaymentElement options prop accept?
It accepts PaymentElementOptions from @stripe/stripe-js, which includes layout, fields, terms, wallets, and other configuration. Import it for type safety when building dynamic configurations.
Does PaymentElement require a clientSecret on the Elements provider?
Yes. Unlike CardElement, PaymentElement requires a clientSecret from a PaymentIntent or SetupIntent passed via the options prop on <Elements>. Without it, PaymentElement will not render.