Skip to main content

Standard storefront actions

Standard storefront actions are async functions on Shopify.actions that apps and agents call to trigger theme behaviors, such as adding to cart, opening the cart drawer, or fetching the current cart. The theme decides how to handle each call, so the caller doesn't need to know whether the theme renders a cart drawer, a cart page, or another UI.

Shopify provides a default implementation for every action. Themes override the defaults to replace the default behavior with their own UI updates.

Standard storefront events fire automatically when a configured action succeeds.


Anchor to How actions are loadedHow actions are loaded

Shopify injects the actions script on every Liquid storefront, so themes don't need to add a <script> tag. After the page loads, actions are available on window.Shopify.actions with default Storefront API-based handlers.


Call actions on Shopify.actions. The promise resolves with the result whether the theme has configured the action or not:

const { cart } = await Shopify.actions.updateCart({
lines: [{ merchandiseId: "gid://shopify/ProductVariant/123", quantity: 1 }],
});

To check whether an action has been configured by the theme, call isDefault():

if (!Shopify.actions.updateCart.isDefault()) {
// The theme has configured this action.
}

Configuring an action replaces the default handler with the theme's own behavior, such as opening a cart drawer or updating a counter in place.

Note

getCart is intentionally not configurable. Calling Shopify.actions.getCart.configure(...) is a TypeScript error and a runtime TypeError.

Without a configuration, each action runs its default behavior:

  • updateCart: Writes to the Storefront API, then attempts an in-place cart refresh, such as a cart:update event on Horizon-style themes or a section-render swap on Dawn-style themes, and falls back to a full page reload if neither pattern matches.
  • openCart: Calls .open() on a <cart-drawer-component> or <cart-drawer> element if either is present, and otherwise redirects to /cart.
  • getCart: Reads the current cart from the Storefront API without affecting the page.

Register a configuration inside a DOMContentLoaded listener placed above {{ content_for_header }} in the layout file, so that the configuration runs before any app code.

A configuration accepts two options: eventTarget and handler.

Anchor to eventTarget (required for updateCart)eventTarget (required for updateCart)

A function that returns the element from which auto-emitted events should dispatch. updateCart is the only action that auto-emits events.

The function receives a meta object with type (the full event name, such as 'shopify:cart:lines-update'). When type is 'shopify:cart:lines-update', meta also includes action ('add', 'remove', or 'update'). Use meta to route different events to different DOM elements:

document.addEventListener('DOMContentLoaded', () => {
Shopify.actions.updateCart.configure({
eventTarget: (meta) => {
if (meta.type === 'shopify:cart:note-update') return document.querySelector('cart-note');
if (meta.type === 'shopify:cart:discount-update') return document.querySelector('cart-discount');
if (meta.type === 'shopify:cart:lines-update' && meta.action === 'add') {
return document.querySelector('product-form');
}
return document.querySelector('cart-items');
},
async handler(defaultHandler, payload, options) {
const result = await defaultHandler();
customUpdateUI(result);
return result;
},
});
});

An async function that runs in place of the default handler. It receives defaultHandler (the Storefront API implementation), payload, and options. To preserve the default behavior and add custom logic, call defaultHandler() and use its result.

If eventTarget is provided without handler, then the default Storefront API handler runs, the page reload is skipped, and auto-emitted events dispatch from the element returned by eventTarget. Use this pattern when the theme already listens for standard events to update its UI.

The example below uses both eventTarget and a custom handler that fetches and replaces section HTML, and registers an openCart configuration in the same listener:

document.addEventListener('DOMContentLoaded', () => {
Shopify.actions.updateCart.configure({
eventTarget: (meta) => {
if (meta.type === 'shopify:cart:lines-update' && meta.action === 'add') {
return document.querySelector('product-form');
}
return document.querySelector('cart-items');
},
async handler(defaultHandler, payload) {
const result = await defaultHandler();
const response = await fetch(window.location.pathname + '?sections=cart-drawer');
const { 'cart-drawer': html } = await response.json();
document.querySelector('cart-drawer').innerHTML = html;
return result;
},
});

Shopify.actions.openCart.configure({
handler() {
document.querySelector('cart-drawer')?.open();
},
});
});

Anchor to Preventing double UI updatesPreventing double UI updates

If a configuration re-renders the cart drawer and the theme's components also re-render when they receive shopify:cart:lines-update, then both render paths run and the UI updates twice. Use detail.source in the resolved promise to identify the source of the update and skip the second render:

// In the configure handler: return a result with a detail field
async handler(defaultHandler, payload) {
const result = await defaultHandler();
return { ...result, detail: { source: 'configure' } };
}

// In the component event listener
event.promise?.then(({ detail }) => {
if (detail?.source === 'configure') return;
morphSection(sectionId);
});

When a configured action succeeds, the matching standard events fire automatically based on what changed:

ActionEvents auto-emitted
updateCartshopify:cart:lines-update (if lines changed), shopify:cart:note-update (if the note changed), shopify:cart:discount-update (if discounts changed), shopify:cart:error (if the mutation fails)
openCartNone
getCartNone

Events dispatch from the element returned by eventTarget. When an action is configured, the theme doesn't need to also dispatch these events from its own code.


The updateCart action promise always resolves with { cart, userErrors?, warnings?, detail? }. It rejects only when the action couldn't run at all, such as a network failure or malformed payload.

A userErrors array indicates the mutation was rejected and cart state didn't change for the input. Common Storefront API codes include INVALID (for a malformed input) and MAXIMUM_EXCEEDED (for a quantity above the item's maximum). See CartErrorCode for the full list.

A warnings array indicates the mutation succeeded but with caveats worth surfacing. The cart did mutate. Common Storefront API codes include MERCHANDISE_OUT_OF_STOCK for a line whose merchandise is now out of stock and DISCOUNT_NOT_FOUND for an unknown discount code. See CartWarningCode for the full list.

Check userErrors and warnings before using cart:

const { cart, userErrors, warnings } = await Shopify.actions.updateCart({
lines: [{ merchandiseId: "gid://shopify/ProductVariant/123", quantity: 1 }],
});

if (userErrors?.length) {
// Mutation rejected. Show userErrors[0].message to the buyer.
return;
}

if (warnings?.length) {
// Mutation succeeded with caveats. Show warnings[0].message.
}

// Use cart for the success path.

Apps and override handlers consume the result with this shape regardless of whether the default Storefront API handler ran or a configured handler did.


Action: Shopify.actions.getCart(payload?, options?)

Retrieves the current cart from the Storefront API, or null if no cart exists. The cartId argument is optional and auto-detected from the cart cookie when omitted. Multiple concurrent calls reuse the same in-flight request.

const { cart } = await Shopify.actions.getCart();

Returns: Promise<{ cart | null }> where cart is a subset of the Storefront API Cart object.

Triggers: no events.

Pass { signal: abortController.signal } as a second argument to cancel the in-flight request.


Action: Shopify.actions.updateCart(payload, options?)

Adds, removes, or updates cart lines. Also handles notes and discount codes. Creates a cart if none exists.

await Shopify.actions.updateCart({
lines: [
{ merchandiseId: "gid://shopify/ProductVariant/123", quantity: 1 },
{ id: "gid://shopify/CartLine/456", quantity: 5 },
{ id: "gid://shopify/CartLine/789", quantity: 0 },
],
note: "Gift wrap please",
discountCodes: ["SAVE10"],
});

Line shapes:

  • To add a new item, pass merchandiseId and quantity.
  • To update an existing line, pass the line's id and the new quantity.
  • To remove a line, pass the line's id with quantity: 0.

Returns: Promise<{ cart, userErrors?, warnings? }>.

Triggers: shopify:cart:lines-update (if lines changed), shopify:cart:note-update (if the note changed), and shopify:cart:discount-update (if discount codes changed).

You can pass options as a second argument. It can include:

  • signal: an AbortSignal that cancels the underlying request.
  • event.detail: an object added to the detail field of any auto-emitted events.
  • event.context: sets the context field of any auto-emitted cart:lines-update or cart:note-update events. Accepts 'product', 'cart', 'dialog', or 'standard-action'. Defaults to 'standard-action'.

Action: Shopify.actions.openCart()

Requests that the theme display the cart UI. The default opens a <cart-drawer-component> or <cart-drawer> element if either is present, and otherwise redirects to /cart. Themes configure this action to open a drawer or modal.

await Shopify.actions.openCart();

Returns: Promise<void>.

Triggers: no events.

Configure example:

Shopify.actions.openCart.configure({
handler() {
document.querySelector('cart-drawer')?.open();
},
});

Was this page helpful?