Build a payment page with multiple Elements
Combine the Express Checkout Element, Payment Element, and Address Element on a single page.
Stripe Elements are modular UI components you can combine to build a complete payment flow on a single page. This guide shows you how to use the Express Checkout Element, Address Element, and Payment Element together to allow customers to pay with one-click wallets or a traditional payment form.
How Elements work together
When you create multiple Elements from the same Checkout instance, they automatically share data and coordinate behavior:
- The Address Element passes address data to actions. confirm() without extra code.
- The Payment Element detects the Address Element and hides redundant billing fields.
- The Express Checkout Element and the Payment Element share the same Checkout Session, so only one confirms the payment per checkout.
| Element | Purpose |
|---|---|
| Express Checkout Element | Displays one-click wallet buttons (Apple Pay, Google Pay, Link, PayPal) |
| Address Element | Collects a shipping or billing address |
| Payment Element | Collects payment method details for traditional payment methods |
When both the Express Checkout Element and the Payment Element are on the same page, wallet payment methods (Apple Pay, Google Pay) only appear in the Express Checkout Element to avoid duplication.
Before you begin
- Register your Stripe account .
- Set up a server that can make requests to the Stripe API.
- Add Stripe.js to your client-side code: <script src="https://js. stripe. com/v3/"></script>
- Review the Checkout Sessions quickstart for a full end-to-end integration example.
Create a Checkout Session
Create a Checkout Session on your server with ui_mode set to elements. The client_secret returned by this request connects all Elements on your page to the same checkout.
Command Line
Select a language
cURL
Stripe CLI
Ruby
Python
PHP
Java
Node.js
Go
.NET
No results
Return the client_secret to your front end. Don’t log it or expose it in a URL.
Initialize Checkout on the client
Initialize Stripe.js and create a Checkout instance using stripe.initCheckoutElementsSdk. All Elements on the page are created from this instance.
const stripe = Stripe('pk_test_TYooMQauvdEDq54NiTphI7jx');
const checkout = stripe.initCheckoutElementsSdk({
clientSecret: '{{CLIENT_SECRET}}',
elementsOptions: {
appearance: {
theme: 'stripe',
},
},
});
The appearance configuration applies consistent styling across all Elements created from this instance. For customization options, see the Appearance API.
Add the Express Checkout Element
Create and mount the Express Checkout Element at the top of your form. It displays one-click wallet buttons when they’re available in the customer’s browser.
<div id="express-checkout-element"></div>
<div id="separator" style="display: none;">
<span>Or pay with card</span>
</div>
const expressCheckoutElement = checkout.createExpressCheckoutElement();
expressCheckoutElement.mount('#express-checkout-element');
Show a separator between wallet buttons and the rest of the form when at least one wallet is available:
expressCheckoutElement.on('ready', ({availablePaymentMethods}) => {
if (availablePaymentMethods) {
document.getElementById('separator').style.display = 'block';
}
});
Add the Address Element
Create and mount a Shipping Address Element below the express checkout section. Mount it before the Payment Element so the Payment Element can detect collected address data and hide redundant fields.
<div id="address-element"></div>
const addressElement = checkout.createShippingAddressElement();
addressElement.mount('#address-element');
The allowed countries are configured server-side through shipping_address_collection.allowed_countries on the Checkout Session. Use createShippingAddressElement() to collect a shipping address, or createBillingAddressElement() for a billing address. With a shipping address, Stripe automatically uses it as the billing address unless the customer provides a separate one through the Payment Element.
Add the Payment Element
Create and mount the Payment Element after the Address Element. It automatically adjusts its billing address fields based on what the Address Element already collected.
<div id="payment-element"></div>
<button id="submit-button">Pay</button>
<div id="error-message"></div>
const paymentElement = checkout.createPaymentElement();
paymentElement.mount('#payment-element');
Handle Express Checkout Element confirmation
When a customer selects a wallet button, the Express Checkout Element fires a confirm event. Handle it by loading actions and calling confirm():
expressCheckoutElement.on('confirm', async (event) => {
const result = await checkout.loadActions();
if (result.type === 'error') {
document.getElementById('error-message').textContent = result.error.message;
return;
}
const {error} = await result.actions.confirm({
expressCheckoutConfirmEvent: event,
});
if (error) {
document.getElementById('error-message').textContent = error.message;
}
});
When the Express Checkout Element confirms the payment, Stripe redirects the customer to your return_url. The wallet provides its own payment method and address, so the Payment Element and Address Element aren’t involved in this flow.
Handle Payment Element form submission
When a customer fills out the Payment Element and selects your submit button, load actions and call confirm():
document.getElementById('submit-button').addEventListener('click', async (event) => {
event.preventDefault();
event.target.disabled = true;
const result = await checkout.loadActions();
if (result.type === 'error') {
document.getElementById('error-message').textContent = result.error.message;
event.target.disabled = false;
return;
}
const {error} = await result.actions.confirm();
if (error) {
document.getElementById('error-message').textContent = error.message;
event.target.disabled = false;
}
});
Calling actions.confirm() automatically includes the address data from the Address Element and the payment details from the Payment Element. You don’t need to extract and pass them manually.
Put it all together
Use this HTML structure as the recommended element order for your payment page:
<form id="payment-form">
<!-- Express wallets at the top -->
<div id="express-checkout-element"></div>
<div id="separator" style="display: none;">
<span>Or pay with card</span>
</div>
<!-- Address before Payment Element -->
<div id="address-element"></div>
<!-- Payment method collection -->
<div id="payment-element"></div>
<button id="submit-button">Pay</button>
<div id="error-message"></div>
</form>
This layout places elements in the following order:
- Express Checkout Element at the top for one-click payments.
- Address Element collects the shipping or billing address.
- Payment Element collects payment details and adjusts its fields based on the Address Element.
Only one confirmation path runs per checkout. If the customer pays with a wallet, the Express Checkout Element handles confirmation. If they use the payment form, the submit button handles it. Both paths use the same Checkout Session.
Test your integration
Use test cards to verify each path:
| Scenario | Test value |
|---|---|
| Successful card payment | Card number 4242 4242 4242 4242 |
| Authentication required | Card number 4000 0025 0000 3155 |
| Card declined | Card number 4000 0000 0000 9995 |
For any test card, use any future expiry date, any three-digit CVC, and any postal code.
To test express wallet flows, use Apple Pay or Google Pay in a supported browser with a test payment method configured.
Verify that:
- The shipping address appears in the Checkout Session’s shipping _ details field after confirmation.
- The Payment Element hides its billing address fields when the Address Element is present.
- Only one confirmation occurs regardless of which path the customer takes.
