Payment Element y Card Element
Receta
Usa PaymentElement para un input de pago unificado que admite tarjetas, billeteras y transferencias bancarias. Usa CardElement cuando solo necesitas pagos con tarjeta y quieres un control más granular a nivel de campo. Estila ambos con la Appearance API de Stripe.
PaymentElement (recomendado en la mayoría de los casos):
// 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 ?? "Error de validación");
setProcessing(false);
return;
}
const { error: confirmError } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: `${window.location.origin}/success` },
});
if (confirmError) {
setError(confirmError.message ?? "Pago fallido");
setProcessing(false);
}
}
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
{error && <p className="text-red-500 mt-2">{error}</p>}
<button type="submit" disabled={!stripe || processing}>
{processing ? "Procesando..." : "Pagar"}
</button>
</form>
);
}CardElement (solo tarjeta):
"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 {
// Envía paymentMethod.id a tu servidor
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}>Pagar</button>
</form>
);
}Ejemplo funcional
Un formulario de pago estilizado que coincide con una app de tema oscuro:
// 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">Cargando...</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">Completar pago</h2>
<Elements
stripe={stripePromise}
options={{
clientSecret,
appearance,
layout: {
type: "tabs",
defaultCollapsed: false,
},
}}
>
<PaymentForm />
</Elements>
</div>
);
}Profundización
Cómo funciona
PaymentElementes un único componente que renderiza dinámicamente campos de entrada para todos los métodos de pago habilitados (tarjetas, Apple Pay, Google Pay, débitos bancarios, etc.). Stripe determina qué métodos mostrar según la moneda de la transacción, el importe y la ubicación del cliente.CardElementrenderiza un input de tarjeta de una sola línea con los campos de número, vencimiento y CVC combinados. Solo admite pagos con tarjeta.- La Appearance API controla el estilo mediante
theme,variablesyrules. Los temas (stripe,night,flat) proporcionan una base; las variables sobrescriben tokens de diseño; las reglas apuntan a selectores similares a CSS para elementos individuales. - Las opciones de layout para PaymentElement controlan cómo se muestran los métodos de pago:
"tabs"los muestra como pestañas seleccionables,"accordion"como secciones expandibles y"auto"deja que Stripe decida. - La recopilación de datos de facturación se puede configurar por campo. Por defecto, PaymentElement recopila los campos requeridos por el método de pago seleccionado.
Variaciones
Layout de acordeón:
<Elements
stripe={stripePromise}
options={{
clientSecret,
layout: { type: "accordion", defaultCollapsed: false, radios: true },
}}
>
<PaymentForm />
</Elements>Controlar la recopilación de datos de facturación:
<PaymentElement
options={{
layout: "tabs",
fields: {
billingDetails: {
name: "auto",
email: "never", // recopila el email tú mismo
phone: "never",
address: "auto",
},
},
}}
/>Campos de tarjeta separados (CardNumber, CardExpiry, CardCvc):
import {
CardNumberElement,
CardExpiryElement,
CardCvcElement,
} from "@stripe/react-stripe-js";
function SplitCardForm() {
return (
<div className="space-y-4">
<div>
<label>Número de tarjeta</label>
<CardNumberElement />
</div>
<div className="flex gap-4">
<div className="flex-1">
<label>Vencimiento</label>
<CardExpiryElement />
</div>
<div className="flex-1">
<label>CVC</label>
<CardCvcElement />
</div>
</div>
</div>
);
}Notas de TypeScript
- Importa
Appearancedesde@stripe/stripe-jspara tipar tu configuración de apariencia. PaymentElementaceptaPaymentElementProps, que incluye una propoptionstipada comoPaymentElementOptions.- Las opciones de
CardElementusanCardElementOptionscon una API de estilo diferente (style.base,style.invalid, etc.) que la Appearance API.
import type { Appearance, LayoutObject } from "@stripe/stripe-js";
const layout: LayoutObject = {
type: "tabs",
defaultCollapsed: false,
};Errores comunes
PaymentElementrequiere unclientSecreten el proveedorElements. No puedes usarlo sin un PaymentIntent o SetupIntent.CardElementusa un sistema de estilo diferente (propstyle) que PaymentElement (Appearance API). No son intercambiables.- No montes
PaymentElementyCardElementen el mismo proveedorElements. Elige uno. - Las
rulesde la Appearance API usan selectores personalizados de Stripe (.Input,.Label,.Tab), no selectores CSS estándar. Consulta la documentación para la lista completa. - PaymentElement muestra métodos de pago según la configuración del PaymentIntent. Si solo ves tarjetas, comprueba que
automatic_payment_methodsesté habilitado y que tu Stripe Dashboard tenga otros métodos configurados. - Las fuentes cargadas mediante
@font-faceen tu CSS no están disponibles automáticamente dentro de Stripe Elements. Pásalas mediante la opciónfontsenElements.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| PaymentElement | Todos los métodos de pago, un solo componente, moderno | Requiere clientSecret, menos control granular |
| CardElement | Simple, input de tarjeta de una sola línea | Solo tarjetas, API de estilo antigua |
| Campos de tarjeta separados | Control total del layout de cada campo | Más JSX, solo tarjetas |
| Stripe Checkout | Cero código de UI | Sale de tu sitio |
Preguntas frecuentes
¿Cuál es la principal diferencia entre PaymentElement y CardElement?
PaymentElementes un componente unificado que admite tarjetas, billeteras, transferencias bancarias y más de 40 métodos de pago en un solo componenteCardElementes un input de una sola línea que solo admite pagos con tarjeta- PaymentElement se recomienda para la mayoría de las integraciones nuevas
¿Cuáles son las opciones de layout disponibles para PaymentElement?
"tabs"-- los métodos de pago se muestran como pestañas seleccionables"accordion"-- los métodos de pago se muestran como secciones expandibles"auto"-- Stripe decide el mejor layout
¿Cómo funciona la Appearance API para estilar Stripe Elements?
- Elige un
themebase ("stripe","night","flat") - Sobrescribe tokens de diseño mediante
variables(colores, fuentes, radio de borde, espaciado) - Apunta a elementos específicos mediante
rulesusando los selectores personalizados de Stripe (.Input,.Label,.Tab)
Trampa: ¿Puedes montar PaymentElement y CardElement en el mismo proveedor Elements?
No. Debes elegir uno u otro dentro de un único proveedor <Elements>. Montar ambos provoca conflictos y comportamiento indefinido.
¿Cómo controlas qué datos de facturación recopila PaymentElement?
<PaymentElement
options={{
fields: {
billingDetails: {
name: "auto",
email: "never",
phone: "never",
address: "auto",
},
},
}}
/>Establece un campo en "never" si lo recopilas tú mismo en tu propio formulario.
¿Cuáles son los componentes de campos de tarjeta separados y cuándo deberías usarlos?
CardNumberElement,CardExpiryElement,CardCvcElement- Úsalos cuando necesites control total del layout de cada campo de tarjeta
- Solo admiten pagos con tarjeta, no otros métodos de pago
¿Por qué PaymentElement podría mostrar solo pagos con tarjeta aunque automatic_payment_methods esté habilitado?
Revisa la configuración de métodos de pago en tu Stripe Dashboard. automatic_payment_methods muestra métodos que están habilitados en el Dashboard y son aplicables a la moneda de la transacción y la ubicación del cliente.
Trampa: ¿Por qué las fuentes personalizadas de tu CSS no funcionan dentro de Stripe Elements?
Stripe Elements se renderizan dentro de iframes y no pueden acceder a fuentes cargadas mediante @font-face en tu CSS. Debes pasar las fuentes explícitamente mediante la opción fonts en el componente <Elements>.
TypeScript: ¿Cómo tipas un objeto de configuración Appearance?
import type { Appearance, LayoutObject } from "@stripe/stripe-js";
const appearance: Appearance = {
theme: "night",
variables: { colorPrimary: "#6366f1" },
};
const layout: LayoutObject = { type: "tabs" };¿Cuál es la diferencia entre el sistema de estilo de CardElement y la Appearance API?
CardElementusa una propstylecon estadosbase,invalidycompletePaymentElementusa la Appearance API contheme,variablesyrules- Los dos sistemas no son intercambiables
TypeScript: ¿Qué tipo acepta la prop options de PaymentElement?
Acepta PaymentElementOptions de @stripe/stripe-js, que incluye layout, fields, terms, wallets y otra configuración. Impórtalo para seguridad de tipos al construir configuraciones dinámicas.
¿PaymentElement requiere un clientSecret en el proveedor Elements?
Sí. A diferencia de CardElement, PaymentElement requiere un clientSecret de un PaymentIntent o SetupIntent pasado mediante la prop options en <Elements>. Sin él, PaymentElement no se renderizará.