Documentation
One reactive cart. From the first add to the next update.
A cart shared across your theme.
$cart gives Alpine components a shared, reactive view of a Shopify cart. Reads, writes, and opening the cart go through Shopify’s standard storefront actions. The theme stays in charge of its HTML and cart presentation.
You need Alpine.js 3 and a Shopify Liquid storefront with Shopify.actions. This is not a headless Storefront API client, a Shopify Admin integration, or a checkout extension.
Install from source
Until a package release is available, build the repository locally. This revision includes the action-readiness and cart-line compatibility fixes.
git clone https://github.com/BillyNoyes/alpinejs-shopify-cart.git
cd alpinejs-shopify-cart
git checkout dce72cd92190cd4f22dc278e2b4a53fd33b4b3ef
npm ci
npm run build
Use theme assets
Copy dist/alpinejs-shopify-cart.min.js into your theme’s assets/ directory. Add Alpine’s CDN build to that directory as alpine.js. Load the plugin before Alpine, and load Alpine only once.
<script defer src="{{ 'alpinejs-shopify-cart.min.js' | asset_url }}"></script>
<script defer src="{{ 'alpine.js' | asset_url }}"></script>
Use a JavaScript bundle
From an application with Alpine installed, import the built ES module using a path relative to your source file. Register the plugin before calling Alpine.start().
import Alpine from 'alpinejs'
import shopifyCart from './vendor/alpinejs-shopify-cart/dist/index.js'
Alpine.plugin(shopifyCart)
Alpine.start()
The plugin automatically starts a cart read during initialization. On the tested revision it defers that read to a task after the relevant DOM initialization work. Check $cart.error if your runtime is unavailable.
Read reactive state
Every $cart reference resolves to Alpine.store('shopifyCart'). Updating the cart in one component updates dependent expressions elsewhere.
<div x-data>
<span x-text="$cart.totalQuantity"></span>
<span x-show="$cart.pending">Updating cart…</span>
</div>
| Property | Meaning |
|---|---|
ready | The initial read attempt finished, including failure. Check error separately. |
cart | Raw Shopify cart result, or null when no cart exists. |
lines | Cart line array. The tested revision also accepts a lines.nodes connection. |
totalQuantity | Quantity across all lines; zero before a cart is available. |
cost | Cart cost, or null. Money amounts are decimal strings with a currencyCode. |
discountCodes | Codes returned by Shopify, including applicability. |
pending | Whether local or observed external operations remain pending. |
pendingCount | Number of tracked operations, including queued local calls. |
pendingOperation | Most recently registered pending operation, or null. |
note / attributes | Last successfully observed values; undefined until known. |
error | Rejected operation details, or null. |
userErrors / warnings | Shopify’s resolved validation errors and non-blocking warnings. |
detail | Custom detail from the most recently applied result. |
A summary, not a product catalog. Standard cart lines expose IDs, quantity, and cost. They do not supply product titles, images, or full merchandise objects. Render those through Liquid or a separate product-data source. Do not divide money amounts by 100.
Change the cart
Mutation methods return the full Shopify result: { cart, userErrors, warnings, detail }. Requests can reject, so catch failures in your component logic. Local calls are queued in order.
Add a variant
Pass a variant ID as merchandiseId, not a cart line ID. A Shopify GID or a documented raw ID can be used.
const result = await this.$cart.add(
{ merchandiseId: this.variantId, quantity: 1 },
{ context: 'product' }
)
if (result.userErrors?.length) return
await this.$cart.open()
Update or remove a line
Use the line ID returned by Shopify. Quantities are absolute targets, not increments. Disable controls while pending if they derive a new quantity from the current snapshot.
await this.$cart.update({ id: line.id, quantity: 2 })
await this.$cart.remove(line.id)
// Both methods also accept arrays.
await this.$cart.remove([firstLine.id, secondLine.id])
Notes, attributes, and discounts
await this.$cart.setNote('Leave at the front desk')
await this.$cart.setAttributes([
{ key: 'Gift wrap', value: 'Yes' }
])
await this.$cart.setDiscountCodes(['WELCOME10'])
Attributes and discount codes replace the complete set. Include every value you want to retain. An empty array clears the set. An empty string clears the note.
Send a complete standard payload
await this.$cart.mutate({
lines: [{ id: line.id, quantity: 2 }],
note: 'Gift order'
}, {
context: 'cart',
detail: { source: 'cart-page' }
})
Refresh and open
await this.$cart.refresh()
await this.$cart.open()
| Method | Input |
|---|---|
add(lines, options?) | One line or an array; merchandiseId, quantity, optional attributes and sellingPlanId. |
update(lines, options?) | One existing line or an array with id and quantity. |
remove(ids, options?) | One line ID or an array; sent with quantity zero. |
setNote(note, options?) | String. |
setAttributes(values, options?) | Complete array of key/value pairs. |
setDiscountCodes(codes, options?) | Complete array of code strings. |
mutate(payload, options?) | Standard updateCart payload. |
refresh(options?) | Optional cartId and signal. Returns { cart }. |
open() | No arguments. Returns Promise<void>. |
Mutation options accept signal, context, detail, and the standard nested event options. Top-level context and detail take precedence over their nested counterparts. Prefer one style consistently.
Receive cart updates
The plugin subscribes to standard events on document. Event properties live directly on the event, including its result promise; they are not all nested under event.detail.
document.addEventListener('shopify:cart:lines-update', event => {
event.promise.then(({ cart, userErrors, warnings }) => {
// Inspect the settled result, not just the initial event.
console.log(cart?.totalQuantity, userErrors, warnings)
}).catch(error => {
console.error(error)
})
})
| Event | Behavior |
|---|---|
shopify:cart:lines-update | Add, update, or remove operation; waits for event.promise. |
shopify:cart:note-update | Retains the requested note after a successful result. |
shopify:cart:attributes-update | Retains attributes after a successful result. |
shopify:cart:discount-update | Reconciles discount codes from the returned cart. |
shopify:cart:error | Records a request failure. |
shopify:cart:view | Reads the cart supplied by the view event. |
Shopify.actions.updateCart() emits its own events. Do not dispatch an additional cart event after using $cart. The plugin tags its own calls to avoid counting them twice.
Changes made outside standard actions are observable only if the theme or app emits the standard events. This plugin does not intercept fetch, poll the cart, or watch arbitrary markup.
Not an analytics channel. These storefront events do not establish tracking consent. Use Shopify Web Pixels for consent-aware analytics.
Handle errors without guessing
There are three distinct outcomes. A rejected promise represents a failed request. userErrors describes a mutation Shopify declined. warnings describes a change that succeeded with an adjustment.
Alpine.data('productForm', variantId => ({
variantId,
message: '',
async add() {
this.message = ''
try {
const result = await this.$cart.add({
merchandiseId: this.variantId,
quantity: 1
})
if (result.userErrors?.length) {
this.message = result.userErrors[0].message
return
}
this.message = result.warnings?.[0]?.message ?? 'Added to cart.'
} catch (error) {
this.message = error.message ?? 'Could not update the cart. Try again.'
}
}
}))
Register this provider before starting Alpine. Wire a button to add(), disable it with $cart.pending, and render message in a live region. The store also records rejected errors; explicit method callers must still handle promise rejection.
Let the theme own the UI
$cart.open() calls Shopify.actions.openCart(). Depending on the theme, that opens a drawer or navigates to the cart page. There is no plugin-owned open state or close method.
Standard actions can reload the page. Shopify’s default update behavior may reload when it cannot recognize the theme’s cart UI. A reactive magic alone does not disable that fallback.
For an in-place experience, the theme must configure an appropriate updateCart handler and event target. Configure a theme-specific openCart handler if needed. This plugin intentionally does not claim those configurations.
Use Shopify’s action configuration reference. Only the first configuration takes effect. Test the actual theme, not just a mocked API, and do not assume wrapping a default handler suppresses its reload.
Lifecycle and limits
The store and its document listeners belong to the page, not to an individual Alpine component. Removing one cart widget must not disconnect other widgets. Register the plugin once before Alpine initializes.
const cart = Alpine.store('shopifyCart')
await cart.refresh()
// Only at application teardown, not when one widget unmounts.
cart.dispose()
dispose() removes listeners and blocks new calls. It is terminal, not a restart API. It does not guarantee that already queued work or server-side mutations are cancelled. Do not use it as a network cancellation mechanism.
The plugin queues local calls and uses local revisions to reject older results. Standard events do not carry a global server revision, so this is not a guarantee against every race involving unrelated scripts. Test overlapping updates in your integration.
- No built-in checkout, product selector, currency formatter, or cart drawer.
- No cross-tab synchronization or legacy Ajax API fallback.
- Notes and attributes are unknown until observed; the standard summary does not hydrate them.
- Use
$cart.linesrather than depending on a single raw line shape.
Go to the source
- Source code and issues
- TypeScript declarations
- Live-store compatibility fixes
- Shopify standard storefront events and actions
- Alpine plugin authoring
Before shipping a theme integration, run Shopify CLI with the standard events inspector and exercise real cart operations, failure paths, and your theme’s cart presentation.
shopify theme dev --standard-events-inspector