Checkout Elements
Build your own checkout layout from frames that match your site, with payment still handled by OpenStock.
Checkout Elements put the OpenStock checkout inside your own page, one piece at a time. You place the cart summary, email field, payment methods and pay button wherever your design wants them and style them to match. Payment still runs on OpenStock: card numbers, codes and orders never touch your page or your server.
<div id="checkout"></div>
<script src="https://openstock.dev/elements.js"></script>
<script>
const elements = OpenStockElements({
store: 'acme',
items: [{ product: 'PRODUCT_ID', quantity: 1 }],
})
elements.create('checkout').mount('#checkout')
</script>Want a modal instead?
How it works#
One group, many elements
OpenStockElements() creates a group for one cart. Every element in the group shares the same email, payment method, coupon and total, so you can split them across your layout.
Each element is a small frame
Elements load from your store's domain (acme.openstock.dev) and size themselves to their content. A hidden frame runs the actual checkout; the elements only show its state and send it what the buyer does.
Payment
The pay element, or your own button calling confirmPayment(), starts the payment. Methods with their own page (card processors, PayPal, crypto) send the buyer there and back to the order page. Balance and gift card payments finish in place and send the buyer to returnUrl with ?order= added.
Elements#
| Type | Shows |
|---|---|
checkout | Everything below in one frame. Pass { showSummary: false } to leave the cart out. |
summary | Products, quantities, discounts and the total. |
email | The address that gets the receipt and the delivery. |
payment | The store's payment methods, including the gift card code field. |
coupon | A coupon field that shows the saving once applied. |
consent | Terms and privacy acceptance, plus the marketing opt-in when the store has it on. |
pay | The pay button with the live amount and anything still missing. |
Each type can be created once per group. create() returns an element with mount(target), unmount(), update(options), focus() and on(event, handler).
Build your own layout#
const elements = OpenStockElements({
store: 'acme',
items: [
{ product: 'PRODUCT_ID', quantity: 2 },
{ product: 'OTHER_ID', variant: 'VARIANT_ID' },
],
locale: 'auto',
})
elements.create('summary').mount('#summary')
elements.create('email').mount('#email')
elements.create('payment').mount('#payment')
elements.create('coupon').mount('#coupon')
elements.create('consent').mount('#consent')
// Your own button instead of the pay element:
document.querySelector('#buy').addEventListener('click', async () => {
const { error, order } = await elements.confirmPayment({
returnUrl: 'https://acme.com/thanks',
})
if (error) showMessage(error.message)
})Options#
| Option | Description |
|---|---|
store | Your store slug, the part before .openstock.dev. Required. |
items | Up to 20 of { product, quantity?, variant? }. Prices always come from your store; quantities are capped at 99. |
brand | Mode, tokens, fonts and rules. See below. |
locale | en, es, fr, de, ja, zh or auto (your page's lang, then the browser). |
returnUrl | Where to go after an in-place payment. Can also be passed to confirmPayment(). |
storeUrl | Your custom domain, if the store has one. |
The group also has update({ brand, items }), edit() to clear a failed payment's message, getState() and destroy().
Branding#
OpenStockElements({
store: 'acme',
items,
brand: {
mode: 'auto', // 'light' | 'dark' | 'auto'
fonts: ['Inter:wght@400;500;600'], // Google Fonts only
tokens: {
accent: '#7c3aed',
radius: '12px',
font: 'Inter, sans-serif',
},
rules: {
'.os-button-primary': { 'border-radius': '999px' },
'.os-method[data-selected]': { 'border-color': '#7c3aed' },
'.os-input:focus': { 'box-shadow': '0 0 0 3px #ede9fe' },
},
},
})
// Restyle later, e.g. when your site switches theme:
elements.update({ brand: { mode: 'dark' } })Tokens#
accent, accentText, text, textMuted, border, surface, surfaceSunken, danger, success, warning, radius, gap, font, fontMono, fontSize. Colors take hex, rgb(), hsl(), oklch() or a named color. When you set accent without accentText, the text on it is picked for contrast.
Rules#
A rule targets one class from the list below, optionally with one state ([data-selected], [data-disabled], [data-loading], [data-invalid], [data-checked], [data-locked]) and one pseudo-class (:hover, :focus, :focus-visible, :focus-within, :active, :disabled, :checked, ::placeholder). Rules always win over the defaults.
.os-element .os-checkout .os-section .os-heading .os-field .os-label .os-input .os-hint .os-error .os-button .os-button-primary .os-button-secondary .os-link .os-summary .os-line .os-line-image .os-line-name .os-line-meta .os-line-price .os-row .os-row-label .os-row-value .os-total .os-total-label .os-total-amount .os-payment .os-methods .os-method .os-method-icon .os-method-label .os-method-detail .os-method-note .os-method-panel .os-coupon .os-coupon-applied .os-consent .os-check .os-check-box .os-check-label .os-pay .os-alert .os-skeleton
What a rule can't do
position, offsets, z-index, display, visibility, opacity, transform, clip-path, masks, filters, content, pointer-events or url(). Text can't be transparent or smaller than 11px, and !important is removed. Anything refused is reported as a trace event and in the console.Fonts load from Google Fonts only: pass Family:wght@400;600, a { family, weights } object or a fonts.googleapis.com URL, and the link is rebuilt from the family and weights. Set preset: 'bare' to drop the default surfaces and borders and style everything yourself.
Events#
elements.on('ready', () => hideSpinner())
elements.on('change', (state) => {
buyButton.disabled = !state.complete
totalLabel.textContent = state.total
})
elements.on('error', (error) => console.warn(error.code, error.message))
elements.on('navigate', (event) => {
// event.url, event.reason: 'payment' | 'complete' | 'hosted-checkout'
// Call event.preventDefault() to handle the navigation yourself.
})
elements.on('trace', ({ message }) => console.debug(message))| Event | When |
|---|---|
ready | Every mounted element and the checkout behind them have loaded. |
change | { complete, empty, email, method, total, currency, missing, loading } |
error | { type, code, message, element, recoverable }. type is validation, payment or load. |
complete | An order was paid without leaving the page (balance or a gift card). |
navigate | The buyer is about to go to a payment page, a delivery page or the hosted checkout. |
resize | An element changed height. Frames resize themselves; this is for your layout. |
focus / blur | Focus moved into or out of an element. |
trace | A brand token, rule or font was refused, with the reason. |
loaderror | An element or the checkout did not load within 15 seconds. |
Good to know#
- Orders with shipped items or a required Discord connection show a button to the full checkout instead of the pay button, because those steps need it.
elements.jsadds no style or script tags to your page, so it works under a strict Content Security Policy. Allowframe-src https://*.openstock.dev(or your custom domain).- Messages from the frames are only accepted from your store's origin, and the frames only talk to each other inside the group.