Mini-cart
How the cart drawer's custom elements share cart state through events, and how they detect cart writes made by other apps.
The cart drawer is the largest piece of custom code in the theme. It is a set of custom elements that never call each other. Each element reads the cart from events on a shared bus.
Why it is built this way
Many scripts on a Shopify storefront write to the same cart. The theme writes to it, and so do the wishlist app, the discount app, the upsell app and any quick-add button on the page. None of them tell the theme when they do.
A drawer that fetched the cart again on every interaction would be slow, and it would still miss writes made while it was closed. The mini-cart instead treats cart updates as a stream of events, and gives each job to exactly one component.
The parts
| Part | File | Responsibility |
|---|---|---|
CartEventBus | internals/event-bus.ts | Wraps CustomEvent on window. Typed by event name |
CartService | internals/cart-service.ts | Calls the Shopify cart endpoints and emits lifecycle events |
SimpleCartService | internals/simple-cart-service.ts | Line-addressed changes, with recovery from stale line keys |
CartStore | internals/cart-store.ts | Holds the last known cart. Loads once, then updates from events |
CartCommandController | internals/command-controller.ts | Turns mini-cart:* events from outside code into service calls |
CartFetchInterceptor | internals/fetch-interceptor.ts | Patches window.fetch and XMLHttpRequest to notice other writers |
| Elements | elements/*.ts | Custom elements that each render one part of the drawer |
internals/core.ts defines every event name, and its CartEventDetailMap types each event's payload. A new event needs
an entry in both.
The command interface
Any code on the page, including code the theme does not own, can change the cart by dispatching an event on window:
window.dispatchEvent(
new CustomEvent('mini-cart:add', {
detail: { items: [{ id: 123456789, quantity: 1 }] },
}),
);The command controller handles mini-cart:get, mini-cart:add, mini-cart:change, mini-cart:update and
mini-cart:clear. Code that sends these events instead of calling /cart/*.js gets the same request queueing and
stale-key recovery as the drawer's own controls.
Why the fetch interceptor exists
The drawer has to find out when an app writes to the cart. The interceptor patches window.fetch and XMLHttpRequest,
watches for POST requests to URLs matching /cart/*.js, and emits cart:changed so the drawer renders again.
Two details matter.
The mini-cart's own requests come back with an x-source: mini-cart response header. The interceptor skips those
responses, so the drawer does not react twice to its own write.
/cart/add.js returns only the added items, not the whole cart. After a successful add, the interceptor requests
GET /cart.js and uses that response as the event payload.
The request and response events are off by default. A comment in the source says other apps send so many cart requests that these events are noise. Turn them on only while debugging.
Stale line item keys
Shopify builds a line item's key from its variant and its properties. Writing a property to a line gives it a new key, even though it is the same line.
This theme writes line item properties automatically, through the line item properties update chain, so keys change more
often here than in most themes. DOM rendered before such a write holds a key the API no longer accepts, and a request
using it fails with 400 no valid id or line parameter.
resolveLineItem in simple-cart-service.ts recovers by matching on the variant ID, which does not change. It only
does so when exactly one line has that variant. If two lines share a variant, such as a purchased item and a free gift
of the same product, it cannot tell them apart. It stops, and the caller renders the cart again instead of guessing.
The rule chains
Two extension points let other code change cart behaviour.
CheckoutEventChain runs a series of async functions over the cart when the shopper clicks checkout. Each function
receives the cart the previous one returned. The footer element owns and runs the chain. Other code adds steps through
window.addCheckoutEventChain, which finds the chain on the mini-cart-footer element, and the free gifts component
registers this way. The footer handles its own shipping protection upsell directly, outside the chain. On a page without
a footer block there is no chain, so nothing else can register a step.
The line item properties update chain collects proposed property changes across the cart and applies them in one pass. Two rules run today.
promotionLabelQuantityRule removes the Promotion property from any line whose quantity drops below 3.
remainingAmountRecalculationRule recalculates the remaining balance on Medispa deposit lines. It applies to any line
whose vendor contains medispa, or that already has a remaining amount property.
A rule returns proposed changes and does not write to the cart. The chain collects the proposals, skips any that match the current values, and sends the rest together. This keeps the number of key-changing requests low.
Failure modes
The drawer shows a stale cart. An app wrote to the cart in a way the interceptor does not match. In the network tab,
look for cart requests that are not a POST to /cart/*.js.
A quantity change returns a 400. The request used a stale line key that resolveLineItem could not match, usually
because the same variant is in the cart twice. See Cart and line item errors.
An element renders nothing. The block is not enabled in the section, or its render location puts it in a different part of the drawer. See Mini-cart and gifts.
Nothing in the drawer works. mini-cart.ts imports the internals/ modules, but they also sit in
src/entrypoints, so Vite emits each one as a separate chunk. If one chunk fails to load, the drawer mounts without its
services.
Source map
| Concern | File |
|---|---|
| Section markup, blocks and settings | sections/mini-cart.liquid |
| Per-block markup | snippets/mini-cart.*.liquid |
| Block dispatch by render location | snippets/mini-cart.render-blocks.liquid |
| Event names and payload types | src/entrypoints/mini-cart/internals/core.ts |
| Elements | src/entrypoints/mini-cart/elements/ |
| A worked example of a new element | src/entrypoints/mini-cart/elements/00-example.ts |
| Rules | src/entrypoints/mini-cart/internals/line-item-rules/ |
| Cart source tagging | src/lib/cart-source.ts |
00-example.ts is a template to copy when you add an element. It is not dead code.