Warning
Alpha — early preview. This software is an early preview and is not
production-ready. Stability is not guaranteed, and breaking changes may
occur in any release. Published under the next dist-tag — see
Installation.
Shopify Checkout Kit is a web component library that enables any website to present the world's highest converting, customizable, one-page checkout. The presented experience is a fully-featured checkout that preserves all of the store customizations: Checkout UI extensions, Functions, branding, and more. It also provides web idiomatic defaults such as opening checkout in a popup or new tab, a transient overlay scrim while the popup is open, and convenient developer APIs to embed, customize, and follow the lifecycle of the checkout experience via the Embedded Checkout Protocol.
Check out our blog to learn how and why we built the Shopify Checkout Kit.
- Platform Requirements
- Getting Started
- Basic Usage
- Programmatic Usage
- Usage with other frameworks
- Usage with the Shopify Storefront API
- Configuration
- Checkout lifecycle
- Explore the sample app
- Contributing
- License
- Browsers — evergreen Chromium, Firefox, and WebKit (Safari 16.4+). The
component relies on
<dialog>, nativecustomElements, andAbortController— all stable in every supported browser. - TypeScript (optional) —
5.0+for consumers using the bundled type definitions. - Bundler (optional) — works with Vite, Rollup, esbuild, webpack, or
no bundler at all via
<script type="module">.
Shopify Checkout Kit for the web is an open-source npm package.
Use the following steps to get started with adding it to your web application:
The package is currently in pre-release. There is no latest dist-tag
published yet, so plain pnpm add @shopify/checkout-kit will fail with
no matching version. Pin to the alpha explicitly, or follow the next
dist-tag:
# Pin to the current alpha (recommended for now — prereleases can change shape)
pnpm add @shopify/checkout-kit@4.0.0-alpha.2
# Or track the latest prerelease via the `next` dist-tag
pnpm add @shopify/checkout-kit@next
# The same works with npm:
npm install @shopify/checkout-kit@4.0.0-alpha.2
# or
npm install @shopify/checkout-kit@nextOnce the first stable 4.0.0 ships, the standard pnpm add @shopify/checkout-kit
(no version specifier) will work and pull from the latest dist-tag.
Import the package once anywhere in your application. The import has a side
effect — it registers <shopify-checkout> with customElements:
import '@shopify/checkout-kit';Then render the element anywhere in your HTML and call open() to present
checkout:
<shopify-checkout
id="checkout"
src="https://your-store.myshopify.com/checkouts/cn/abc123"
target="popup"
></shopify-checkout>
<button id="buy-now">Buy now</button>
<script type="module">
import '@shopify/checkout-kit';
const checkout = document.getElementById('checkout');
document.getElementById('buy-now').addEventListener('click', () => {
checkout.open();
});
</script>The element has no visible layout of its own beyond a transient <dialog>
scrim that appears over the host page while the popup is open. It can sit
anywhere in your DOM.
See usage with the Storefront API below for details on how to obtain a checkout URL.
If you'd rather not declare the element in HTML, create one from JavaScript:
import '@shopify/checkout-kit';
import type {ShopifyCheckout} from '@shopify/checkout-kit';
const checkout = document.createElement('shopify-checkout') as ShopifyCheckout;
checkout.src = 'https://your-store.myshopify.com/checkouts/cn/abc123';
checkout.target = 'popup';
document.body.append(checkout);
checkout.addEventListener('ec.complete', (event) => {
console.log('Order complete', event.detail.checkout.order?.id);
});
checkout.open();
// Later:
checkout.close();The ShopifyCheckout class is also exported directly if you want to import
the constructor without registering the element globally:
import {ShopifyCheckout} from '@shopify/checkout-kit';
if (!customElements.get('shopify-checkout')) {
customElements.define('shopify-checkout', ShopifyCheckout);
}React 19+ has first-class support for custom elements — it renders
<shopify-checkout> and forwards props to it as properties with no extra
configuration. Reach for a ref for the two things that aren't expressible as
JSX props: calling imperative methods (open(), close(), focus()) and
subscribing to the ec.* events.
import {useEffect, useRef} from 'react';
import '@shopify/checkout-kit';
import type {ShopifyCheckout} from '@shopify/checkout-kit';
export function BuyNowButton({checkoutUrl}: {checkoutUrl: string}) {
const checkoutRef = useRef<ShopifyCheckout>(null);
useEffect(() => {
const checkout = checkoutRef.current;
if (!checkout) return;
// A single AbortController removes every listener on cleanup.
const controller = new AbortController();
const {signal} = controller;
checkout.addEventListener(
'ec.complete',
(event) => console.log('Order complete', event.detail.checkout.order?.id),
{signal},
);
checkout.addEventListener('ec.close', () => console.log('Dismissed'), {
signal,
});
return () => controller.abort();
}, []);
return (
<>
<shopify-checkout ref={checkoutRef} src={checkoutUrl} target="popup" />
<button onClick={() => checkoutRef.current?.open()}>Buy now</button>
</>
);
}event is fully typed inside each listener. For example, order data for
ec.complete is available at event.detail.checkout.order. The element's
overloaded addEventListener signatures provide these types. See
Checkout lifecycle for the full event list.
TypeScript doesn't know about the <shopify-checkout> tag in JSX out of the
box. Declare it once, anywhere in your project's type definitions:
import type {ShopifyCheckout} from '@shopify/checkout-kit';
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'shopify-checkout': DetailedHTMLProps<
HTMLAttributes<ShopifyCheckout>,
ShopifyCheckout
> & {
src?: string;
target?: string;
appearance?: string;
'log-level'?: 'debug' | 'warn' | 'error' | 'none';
};
}
}
}Note
On React 18 and earlier, declare the element on the global JSX
namespace (declare global { namespace JSX { ... } }) instead of augmenting
the react module.
Note
The import '@shopify/checkout-kit' side effect registers the element with
customElements and touches browser-only globals, so it must run on the
client. In server-rendered frameworks (Next.js, Remix), keep the import and
the component in a client component — e.g. add 'use client' to the top of
the file.
To present checkout you first need a checkout URL. The most common way is to
use the Storefront GraphQL API to
assemble a cart (via cartCreate and related mutations) and read the
checkoutUrl
field. Alternatively, a
cart permalink
can be provided.
const response = await fetch(
'https://your-store.myshopify.com/api/2026-04/graphql.json',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token': '<storefront access token>',
},
body: JSON.stringify({
query: /* GraphQL */ `
mutation CreateCart($lines: [CartLineInput!]) {
cartCreate(input: {lines: $lines}) {
cart {
id
checkoutUrl
}
userErrors {
field
message
}
}
}
`,
variables: {
lines: [{merchandiseId: 'gid://shopify/ProductVariant/...', quantity: 1}],
},
}),
},
);
if (!response.ok) {
throw new Error(`Storefront API request failed: ${response.status}`);
}
const {data, errors} = await response.json();
if (errors?.length || data.cartCreate.userErrors.length) {
throw new Error('Could not create cart');
}
checkout.src = data.cartCreate.cart.checkoutUrl;For production use, see the
Storefront API GraphiQL Explorer
for schema exploration and the
cartCreate
mutation reference for the full input shape (buyer identity, attributes,
discount codes, delivery preferences, etc.).
Important
src must be an https: URL. The component drops invalid or non-HTTPS
values and refuses to open. At log-level="warn" or more verbose, a
warning is logged to the console.
The presented checkout is customized via attributes on the
<shopify-checkout> element (or the equivalent properties on the
ShopifyCheckout instance — both are reflected).
The URL of the checkout to load. Typically cart.checkoutUrl from the
Storefront API.
<shopify-checkout src="https://your-store.myshopify.com/checkouts/cn/abc123" />checkout.src = 'https://your-store.myshopify.com/checkouts/cn/abc123';The component appends a handful of query parameters to src when it opens
checkout: ec_version (Embedded Checkout Protocol version),
ec_delegate (which capabilities the host delegates), and ck_version
(the Checkout Kit version).
Where the checkout is presented. Defaults to "auto".
| Value | Behavior |
|---|---|
"auto" |
Opens checkout in a new browser tab (default). |
"popup" |
Opens checkout in a popup window sized and centered over the page. |
"_blank" |
Synonym for "auto" — new tab. |
| (string) | Any other value is treated as a named window target, the same as the target parameter of window.open(). |
<shopify-checkout target="popup" />Note
"_self", "_parent", and "_top" are not allowed — they would navigate
the host page away. The component falls back to "auto" if you set one,
and logs a warning at log-level="warn" or more verbose.
Sets the checkout appearance preference. Defaults to "storefront".
| Value | Behavior |
|---|---|
| (unset) | Uses the storefront's configured checkout branding. |
"app:light" |
Uses the app appearance with the light color scheme. |
"app:dark" |
Uses the app appearance with the dark color scheme. |
"app:automatic" |
Uses the app appearance and lets checkout choose. |
"storefront" |
Uses the storefront's configured checkout branding. |
<shopify-checkout appearance="app:dark" />checkout.appearance = 'app:dark';
checkout.appearance = undefined;Invalid values are ignored. At log-level="warn" or more verbose, the
component logs a warning for invalid values.
Controls console logging verbosity. Defaults to "error". The levels form a
threshold ordered from most to least verbose — selecting one emits that level
and every more-severe level:
| Value | Emits |
|---|---|
"debug" |
debug, warnings, and errors |
"warn" |
warnings and errors |
"error" |
errors only (default) |
"none" |
nothing |
Use "debug" while wiring up src and event handlers during integration, or
"error" / "none" to quiet the integration warnings in production.
<shopify-checkout src="..." log-level="debug" />checkout.logLevel = 'debug';When target="popup", the popup is centered over the host window. Defaults
are 600 × 600, capped at 90% of the host window. Override via CSS custom
properties:
shopify-checkout {
--shopify-checkout-dialog-width: 720;
--shopify-checkout-dialog-height: 800;
}While a popup is open the component renders a <dialog> scrim over the host
page, with a "Continue your purchase in the checkout window" link and a close
button. Hide it by either:
- Setting
display: noneon the element itself, or - Targeting the
overlayshadow part:
shopify-checkout::part(overlay) {
display: none;
}The element dispatches ec.* CustomEvents at every meaningful moment
of the checkout session. All events bubble, so you can listen anywhere in your
DOM — including a single delegated listener at document if you have many
elements on the page. Each event carries a typed event.detail payload with
exactly the fields relevant to that moment.
| Event | event.detail |
When it fires |
|---|---|---|
ec.start |
{checkout} |
Checkout has loaded and is interactive. |
ec.complete |
{checkout} |
The buyer completed the order successfully. |
ec.close |
(none) | The popup was dismissed (by the buyer, by close(), or by focus loss). |
ec.error |
{error} |
Session-level fatal error — tear down the embedded context. |
ec.line_items.change |
{checkout} |
The cart's line items changed (item added/removed/quantity updated). |
ec.totals.change |
{checkout} |
The cart totals changed (subtotal, tax, shipping, discounts, total). |
ec.messages.change |
{checkout} |
Checkout-level warnings/errors/info shown inside the checkout changed. |
ec.start, ec.complete, and the change events carry the full UCP Checkout
snapshot in event.detail.checkout for handlers that need broader context.
checkout.addEventListener('ec.complete', (event) => {
const {order} = event.detail.checkout;
if (order) {
analytics.track('checkout_complete', {orderId: order.id});
}
});
checkout.addEventListener('ec.totals.change', (event) => {
miniCart.updateTotals(event.detail.checkout.totals);
});
checkout.addEventListener('ec.close', () => {
router.back();
});Because these events carry the full snapshot, one handler can combine fields.
For example, rendering an inline cart summary on ec.start requires line
items, totals, and currency together:
checkout.addEventListener('ec.start', (event) => {
const {checkout: snapshot} = event.detail;
loadingSpinner.hide();
cartSummary.render({
currency: snapshot.currency,
items: snapshot.lineItems,
totals: snapshot.totals,
});
});The latest full UCP Checkout snapshot is also mirrored to element.checkout
whenever an event with {checkout} arrives. The latest error is mirrored to
element.error when ec.error fires. These properties are useful for handlers
that don't have a reference to the originating event. TypeScript users get
fully typed events through overloaded addEventListener signatures with no
additional setup.
Note
Most public ec.* DOM event names mirror the underlying
Embedded Checkout Protocol
JSON-RPC method names. ec.close is component-only and synthetic; it is not
part of the ECP wire protocol.
See the sample/ directory for a small Vite playground that mounts
the real <shopify-checkout> element next to a faux storefront. Run it from
this directory with:
pnpm install
pnpm sampleThen open the dev server URL and paste a valid checkout URL into the src
field to try open() / close() / focus() and see the live event stream.
We welcome code contributions, feature requests, and reporting of issues. Please see guidelines and instructions.
See RELEASING.md for the day-to-day publish flow, tag conventions, and one-time setup notes.
Shopify's Checkout Kit is provided under an MIT License.