Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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>
);
}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>
);
}PaymentElement is 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.CardElement renders a single-line card input with number, expiry, and CVC fields combined. It only supports card payments.theme, variables, and rules. Themes (stripe, night, flat) provide a base; variables override design tokens; rules target specific CSS-like selectors for individual elements."tabs" shows them as selectable tabs, "accordion" shows them as expandable sections, and "auto" lets Stripe decide.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>
);
}Appearance from @stripe/stripe-js to type your appearance configuration.PaymentElement accepts PaymentElementProps which includes an options prop typed as PaymentElementOptions.CardElement options use CardElementOptions with 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,
};PaymentElement requires a clientSecret on the Elements provider. You cannot use it without a PaymentIntent or SetupIntent.CardElement uses a different styling system (style prop) than PaymentElement (Appearance API). They are not interchangeable.PaymentElement and CardElement in the same Elements provider. Choose one.rules use Stripe's custom selectors (.Input, .Label, .Tab), not standard CSS selectors. Check the docs for the full list.automatic_payment_methods is enabled and your Stripe Dashboard has other methods configured.@font-face in your CSS are not automatically available inside Stripe Elements. Pass them via the fonts option on Elements.| 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 |
PaymentElement is a unified component that supports cards, wallets, bank transfers, and 40+ payment methods in one componentCardElement is a single-line input that only supports card payments"tabs" -- payment methods shown as selectable tabs"accordion" -- payment methods shown as expandable sections"auto" -- Stripe decides the best layouttheme ("stripe", "night", "flat")variables (colors, fonts, border radius, spacing)rules using Stripe's custom selectors (.Input, .Label, .Tab)No. You must choose one or the other within a single <Elements> provider. Mounting both causes conflicts and undefined behavior.
<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.
CardNumberElement, CardExpiryElement, CardCvcElementCheck 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.
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.
import type { Appearance, LayoutObject } from "@stripe/stripe-js";
const appearance: Appearance = {
theme: "night",
variables: { colorPrimary: "#6366f1" },
};
const layout: LayoutObject = { type: "tabs" };CardElement uses a style prop with base, invalid, and complete statesPaymentElement uses the Appearance API with theme, variables, and rulesIt accepts PaymentElementOptions from @stripe/stripe-js, which includes layout, fields, terms, wallets, and other configuration. Import it for type safety when building dynamic configurations.
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.
Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥