Overview
Cart components handle all cart-related functionality including displaying items, updating quantities, managing discounts, and showing cart state.
CartItemsComponent
Displays and manages cart items with real-time updates.
Class Definition
class CartItemsComponent extends Component<Refs> {
connectedCallback(): void;
disconnectedCallback(): void;
onLineItemRemove(line: number): void;
updateQuantity(config: { line: number; quantity: number; action: string }): void;
handleDiscountUpdate(event: DiscountUpdateEvent): void;
get sectionId(): string;
get isDrawer(): boolean;
}
Refs
Array of quantity selector elements for each cart item
Array of cart item row elements
Element displaying the cart total
Methods
updateQuantity()
Updates the quantity of a cart line item.
Line item number (1-indexed)
Action type: 'change' or 'clear'
updateQuantity(config) {
const cartPerformaceUpdateMarker = cartPerformance.createStartingMarker(`${config.action}:user-action`);
this.#disableCartItems();
const { line, quantity } = config;
const { cartTotal } = this.refs;
const cartItemsComponents = document.querySelectorAll('cart-items-component');
const sectionsToUpdate = new Set([this.sectionId]);
cartItemsComponents.forEach((item) => {
if (item instanceof HTMLElement && item.dataset.sectionId) {
sectionsToUpdate.add(item.dataset.sectionId);
}
});
const body = JSON.stringify({
line: line,
quantity: quantity,
sections: Array.from(sectionsToUpdate).join(','),
sections_url: window.location.pathname,
});
cartTotal?.shimmer();
fetch(`${Theme.routes.cart_change_url}`, fetchConfig('json', { body }))
.then((response) => response.text())
.then((responseText) => {
const parsedResponseText = JSON.parse(responseText);
if (parsedResponseText.errors) {
this.#handleCartError(line, parsedResponseText);
return;
}
// Update cart and dispatch events
// ...
})
.finally(() => {
this.#enableCartItems();
cartPerformance.measureFromMarker(cartPerformaceUpdateMarker);
});
}
Usage:
const cartItems = document.querySelector('cart-items-component');
// Change quantity to 3
cartItems.updateQuantity({
line: 1,
quantity: 3,
action: 'change'
});
// Remove item
cartItems.updateQuantity({
line: 2,
quantity: 0,
action: 'clear'
});
onLineItemRemove()
Removes a line item from the cart with animation.
onLineItemRemove(line) {
this.updateQuantity({
line,
quantity: 0,
action: 'clear',
});
const cartItemRowToRemove = this.refs.cartItemRows[line - 1];
if (!cartItemRowToRemove) return;
const rowsToRemove = [
cartItemRowToRemove,
// Get all nested lines of the row to remove
...this.refs.cartItemRows.filter((row) =>
row.dataset.parentKey === cartItemRowToRemove.dataset.key
),
];
// If the cart item row is the last row, optimistically trigger the cart empty state
const isEmptyCart = rowsToRemove.length == this.refs.cartItemRows.length;
if (isEmptyCart) {
// Show empty cart template
} else {
// Animate removal
rowsToRemove.forEach((row) => {
row.style.setProperty('--row-height', `${row.clientHeight}px`);
row.classList.add('removing');
onAnimationEnd(row, () => row.remove());
});
}
}
Events
The component listens for and dispatches cart-related events:
Listens:
ThemeEvents.cartUpdate - Updates from other cart components
ThemeEvents.discountUpdate - Discount code changes
ThemeEvents.quantitySelectorUpdate - Quantity changes from selectors
Dispatches:
CartUpdateEvent - When cart is successfully updated
Properties
The Shopify section ID for this cart componentget sectionId() {
const { sectionId } = this.dataset;
if (!sectionId) throw new Error('Section id missing');
return sectionId;
}
Whether this component is used in a drawerget isDrawer() {
return this.dataset.drawer !== undefined;
}
Example
<cart-items-component data-section-id="cart-main" data-drawer>
<table>
<tr ref="cartItemRows[]" data-key="variant-123">
<td>
<quantity-selector-component ref="quantitySelectors[]">
<!-- Quantity selector -->
</quantity-selector-component>
</td>
</tr>
</table>
<text-component ref="cartTotal">$99.00</text-component>
</cart-items-component>
CartQuantitySelectorComponent
Quantity selector specialized for cart contexts, using absolute maximum values.
Class Definition
class CartQuantitySelectorComponent extends QuantitySelectorComponent {
getEffectiveMax(): number | null;
updateButtonStates(): void;
}
Methods
getEffectiveMax()
Returns the absolute maximum allowed in cart (not adjusted for current cart quantity).
component-cart-quantity-selector.js
getEffectiveMax() {
const { max } = this.getCurrentValues();
return max; // Cart uses absolute max, not max minus cart quantity
}
updateButtonStates()
Updates plus/minus button states based on current value and limits.
component-cart-quantity-selector.js
updateButtonStates() {
const { minusButton, plusButton } = this.refs;
const { min, value } = this.getCurrentValues();
const effectiveMax = this.getEffectiveMax();
// Cart buttons are always dynamically managed
minusButton.disabled = value <= min;
plusButton.disabled = effectiveMax !== null && value >= effectiveMax;
}
Usage
<cart-quantity-selector-component>
<button ref="minusButton">-</button>
<input
ref="quantityInput"
type="number"
min="1"
max="10"
data-cart-line="1"
/>
<button ref="plusButton">+</button>
</cart-quantity-selector-component>
CartDrawerComponent
Manages the slide-out cart drawer with history integration.
Class Definition
class CartDrawerComponent extends DialogComponent {
open(): void;
close(): void;
}
Refs
dialog
HTMLDialogElement
required
The native dialog element
Methods
open()
Opens the cart drawer and manages installments dialog.
open() {
this.showDialog();
// Close cart drawer when installments CTA is clicked to avoid overlapping dialogs
customElements.whenDefined('shopify-payment-terms').then(() => {
const installmentsContent = document.querySelector('shopify-payment-terms')?.shadowRoot;
const cta = installmentsContent?.querySelector('#shopify-installments-cta');
cta?.addEventListener('click', this.closeDialog, { once: true });
});
}
close()
Closes the cart drawer.
close() {
this.closeDialog();
}
Features
History Integration:
- Pushes history state when opened on mobile
- Closes on back button press
- Cleans up history state on close
Sticky Summary:
- Automatically calculates if cart summary should stick to bottom
- Based on summary height vs drawer height ratio
Auto-open:
- Opens automatically when items are added (if
auto-open attribute is set)
Events
Listens:
CartAddEvent - Auto-opens if configured
DialogOpenEvent - Updates sticky state and manages history
DialogCloseEvent - Cleans up history state
Example
<cart-drawer-component auto-open>
<dialog ref="dialog" class="cart-drawer">
<div class="cart-drawer__content">
<cart-items-component data-drawer>
<!-- Cart items -->
</cart-items-component>
</div>
<div class="cart-drawer__summary">
<!-- Cart totals and checkout button -->
</div>
</dialog>
</cart-drawer-component>
CartIcon
Displays cart icon with animated item count bubble.
Class Definition
class CartIcon extends Component<Refs> {
get currentCartCount(): number;
set currentCartCount(value: number);
onCartUpdate(event: CartUpdateEvent): void;
renderCartBubble(itemCount: number, comingFromProductForm: boolean, animate?: boolean): void;
ensureCartBubbleIsCorrect(): void;
}
Refs
The cart count bubble element
Element displaying the count number
Methods
renderCartBubble()
Updates the cart count with animation.
If true, increments current count; if false, sets absolute count
Whether to animate the change
renderCartBubble = async (itemCount, comingFromProductForm, animate = true) => {
this.refs.cartBubbleCount.classList.toggle('hidden', itemCount === 0);
this.refs.cartBubble.classList.toggle('visually-hidden', itemCount === 0);
this.currentCartCount = comingFromProductForm
? this.currentCartCount + itemCount
: itemCount;
this.classList.toggle('header-actions__cart-icon--has-cart', itemCount > 0);
// Store in sessionStorage for persistence
sessionStorage.setItem('cart-count', JSON.stringify({
value: String(this.currentCartCount),
timestamp: Date.now(),
}));
if (!animate || itemCount === 0) return;
// Animate bubble
await new Promise((resolve) => requestAnimationFrame(resolve));
this.refs.cartBubble.classList.add('cart-bubble--animating');
await onAnimationEnd(this.refs.cartBubbleText);
this.refs.cartBubble.classList.remove('cart-bubble--animating');
};
ensureCartBubbleIsCorrect()
Validates cart count against sessionStorage on page load.
ensureCartBubbleIsCorrect = () => {
const sessionStorageCount = sessionStorage.getItem('cart-count');
if (sessionStorageCount === null) return;
const visibleCount = this.refs.cartBubbleCount.textContent;
try {
const { value, timestamp } = JSON.parse(sessionStorageCount);
if (value === visibleCount) return;
// Only update if timestamp is recent (within 10 seconds)
if (Date.now() - timestamp < 10000) {
const count = parseInt(value, 10);
if (count >= 0) {
this.renderCartBubble(count, false, false);
}
}
} catch (_) {
// Invalid JSON, ignore
}
};
Properties
Current cart item countget currentCartCount() {
return parseInt(this.refs.cartBubbleCount.textContent ?? '0', 10);
}
set currentCartCount(value) {
this.refs.cartBubbleCount.textContent = value < 100 ? String(value) : '';
}
Events
Listens:
ThemeEvents.cartUpdate - Updates count when cart changes
pageshow - Validates count when page restored from cache
Example
<cart-icon>
<a href="/cart">
<svg><!-- Cart icon --></svg>
<div ref="cartBubble" class="cart-bubble">
<span ref="cartBubbleText">
<span ref="cartBubbleCount">3</span>
</span>
</div>
</a>
</cart-icon>
CartNote
Manages customer cart note with debounced updates.
Class Definition
class CartNote extends Component {
updateCartNote(event: InputEvent): void;
}
Methods
updateCartNote()
Debounced handler for cart note updates (200ms delay).
updateCartNote = debounce(async (event) => {
if (!(event.target instanceof HTMLTextAreaElement)) return;
const note = event.target.value;
if (this.#activeFetch) {
this.#activeFetch.abort();
}
const abortController = new AbortController();
this.#activeFetch = abortController;
try {
const config = fetchConfig('json', {
body: JSON.stringify({ note }),
});
await fetch(Theme.routes.cart_update_url, {
...config,
signal: abortController.signal,
});
} catch (error) {
// Handle error
} finally {
this.#activeFetch = null;
cartPerformance.measureFromEvent('note-update:user-action', event);
}
}, 200);
Example
<cart-note>
<label for="cart-note">Order notes</label>
<textarea
id="cart-note"
on:input="updateCartNote"
placeholder="Add special instructions..."
></textarea>
</cart-note>
CartDiscount
Manages discount code application and removal.
Class Definition
class CartDiscount extends Component<Refs> {
applyDiscount(event: SubmitEvent): void;
removeDiscount(event: MouseEvent | KeyboardEvent): void;
}
Refs
General error message container
cartDiscountErrorDiscountCode
Invalid discount code error message
cartDiscountErrorShipping
Shipping discount error message
Methods
applyDiscount()
Applies a discount code to the cart.
applyDiscount = async (event) => {
event.preventDefault();
event.stopPropagation();
const form = event.target;
if (!(form instanceof HTMLFormElement)) return;
const discountCode = form.querySelector('input[name="discount"]');
if (!(discountCode instanceof HTMLInputElement)) return;
const discountCodeValue = discountCode.value;
const existingDiscounts = this.#existingDiscounts();
if (existingDiscounts.includes(discountCodeValue)) return;
// Hide errors
this.refs.cartDiscountError.classList.add('hidden');
const config = fetchConfig('json', {
body: JSON.stringify({
discount: [...existingDiscounts, discountCodeValue].join(','),
sections: [this.dataset.sectionId],
}),
});
const response = await fetch(Theme.routes.cart_update_url, config);
const data = await response.json();
// Check if discount is invalid
if (data.discount_codes.find(d =>
d.code === discountCodeValue && d.applicable === false
)) {
discountCode.value = '';
this.#handleDiscountError('discount_code');
return;
}
// Update cart
document.dispatchEvent(new DiscountUpdateEvent(data, this.id));
morphSection(this.dataset.sectionId, data.sections[this.dataset.sectionId]);
};
removeDiscount()
Removes a discount code from the cart.
removeDiscount = async (event) => {
event.preventDefault();
event.stopPropagation();
const pill = event.target.closest('.cart-discount__pill');
if (!(pill instanceof HTMLLIElement)) return;
const discountCode = pill.dataset.discountCode;
if (!discountCode) return;
const existingDiscounts = this.#existingDiscounts();
const index = existingDiscounts.indexOf(discountCode);
if (index === -1) return;
existingDiscounts.splice(index, 1);
const config = fetchConfig('json', {
body: JSON.stringify({
discount: existingDiscounts.join(','),
sections: [this.dataset.sectionId]
}),
});
const response = await fetch(Theme.routes.cart_update_url, config);
const data = await response.json();
document.dispatchEvent(new DiscountUpdateEvent(data, this.id));
morphSection(this.dataset.sectionId, data.sections[this.dataset.sectionId]);
};
Events
Dispatches:
DiscountUpdateEvent - When discount is applied or removed
Example
<cart-discount-component data-section-id="cart-main">
<form on:submit="applyDiscount">
<input type="text" name="discount" placeholder="Discount code" />
<button type="submit">Apply</button>
</form>
<div ref="cartDiscountError" class="hidden">
<span ref="cartDiscountErrorDiscountCode" class="hidden">
Invalid discount code
</span>
<span ref="cartDiscountErrorShipping" class="hidden">
Shipping discount not applicable
</span>
</div>
<ul>
<li class="cart-discount__pill" data-discount-code="SAVE10">
SAVE10
<button on:click="removeDiscount">×</button>
</li>
</ul>
</cart-discount-component>