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.

Terminal
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.

layout/theme.liquid
<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().

theme.js
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.

Cart count
<div x-data>
  <span x-text="$cart.totalQuantity"></span>
  <span x-show="$cart.pending">Updating cart…</span>
</div>
Cart state
PropertyMeaning
readyThe initial read attempt finished, including failure. Check error separately.
cartRaw Shopify cart result, or null when no cart exists.
linesCart line array. The tested revision also accepts a lines.nodes connection.
totalQuantityQuantity across all lines; zero before a cart is available.
costCart cost, or null. Money amounts are decimal strings with a currencyCode.
discountCodesCodes returned by Shopify, including applicability.
pendingWhether local or observed external operations remain pending.
pendingCountNumber of tracked operations, including queued local calls.
pendingOperationMost recently registered pending operation, or null.
note / attributesLast successfully observed values; undefined until known.
errorRejected operation details, or null.
userErrors / warningsShopify’s resolved validation errors and non-blocking warnings.
detailCustom 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.

Add a product
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.

Line operations
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

Cart metadata
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

Advanced mutation
await this.$cart.mutate({
  lines: [{ id: line.id, quantity: 2 }],
  note: 'Gift order'
}, {
  context: 'cart',
  detail: { source: 'cart-page' }
})

Refresh and open

Read and present
await this.$cart.refresh()
await this.$cart.open()
Method reference
MethodInput
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.

Observe a standard event
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)
  })
})
Observed events
EventBehavior
shopify:cart:lines-updateAdd, update, or remove operation; waits for event.promise.
shopify:cart:note-updateRetains the requested note after a successful result.
shopify:cart:attributes-updateRetains attributes after a successful result.
shopify:cart:discount-updateReconciles discount codes from the returned cart.
shopify:cart:errorRecords a request failure.
shopify:cart:viewReads 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 product form
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.

Access from JavaScript
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.lines rather than depending on a single raw line shape.

Go to the source

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 development server
shopify theme dev --standard-events-inspector