Skip to main content

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

quantitySelectors
HTMLElement[]
Array of quantity selector elements for each cart item
cartItemRows
HTMLTableRowElement[]
Array of cart item row elements
cartTotal
TextComponent
Element displaying the cart total

Methods

updateQuantity()

Updates the quantity of a cart line item.
config.line
number
required
Line item number (1-indexed)
config.quantity
number
required
New quantity value
config.action
string
required
Action type: 'change' or 'clear'
component-cart-items.js
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.
component-cart-items.js
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

sectionId
string
The Shopify section ID for this cart component
get sectionId() {
  const { sectionId } = this.dataset;
  if (!sectionId) throw new Error('Section id missing');
  return sectionId;
}
isDrawer
boolean
Whether this component is used in a drawer
get 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.
cart-drawer.js
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.
cart-drawer.js
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

cartBubble
HTMLElement
required
The cart count bubble element
cartBubbleText
HTMLElement
required
Text inside the bubble
cartBubbleCount
HTMLElement
required
Element displaying the count number

Methods

renderCartBubble()

Updates the cart count with animation.
itemCount
number
required
Total items in cart
comingFromProductForm
boolean
required
If true, increments current count; if false, sets absolute count
animate
boolean
default:"true"
Whether to animate the change
cart-icon.js
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.
cart-icon.js
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

currentCartCount
number
Current cart item count
get 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).
cart-note.js
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

cartDiscountError
HTMLElement
required
General error message container
cartDiscountErrorDiscountCode
HTMLElement
required
Invalid discount code error message
cartDiscountErrorShipping
HTMLElement
required
Shipping discount error message

Methods

applyDiscount()

Applies a discount code to the cart.
cart-discount.js
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.
cart-discount.js
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>

Build docs developers (and LLMs) love