Removing Items, Notes, and Discounts
Implement item removal, cart notes, cart attributes, and display discount information in the cart.
Beyond quantity updates, carts need removal buttons, customer notes, and clear discount displays. Let’s implement these essential features.
Removing Items
Setting Quantity to Zero
The simplest removal method:
async function removeItem(key) { await fetch('/cart/change.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: key, quantity: 0, }), });}Form-Based Removal
<form action="{{ routes.cart_change_url }}" method="post"> <input type="hidden" name="id" value="{{ item.key }}"> <input type="hidden" name="quantity" value="0"> <button type="submit" aria-label="Remove {{ item.product.title }}"> Remove </button></form>Remove Button Component
{# snippets/cart-remove-button.liquid #}
<remove-button class="cart-remove" data-key="{{ item.key }}"> <button type="button" class="cart-remove__button" aria-label="Remove {{ item.product.title }} from cart" > {% render 'icon-trash' %} <span class="cart-remove__text">Remove</span> </button></remove-button>class RemoveButton extends HTMLElement { connectedCallback() { this.button = this.querySelector('button'); this.key = this.dataset.key;
this.button.addEventListener('click', () => this.remove()); }
async remove() { const item = this.closest('.cart-item');
// Show removing state item.classList.add('is-removing'); this.button.disabled = true;
try { const response = await fetch('/cart/change.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: this.key, quantity: 0, }), });
const cart = await response.json();
// Animate out item.style.height = `${item.offsetHeight}px`; item.offsetHeight; // Force reflow item.style.height = '0'; item.style.opacity = '0'; item.style.marginBottom = '0'; item.style.paddingTop = '0'; item.style.paddingBottom = '0';
setTimeout(() => { item.remove();
// Update cart totals document.dispatchEvent( new CustomEvent('cart:updated', { detail: { cart }, }) );
// Check if cart is empty if (cart.item_count === 0) { document.dispatchEvent(new CustomEvent('cart:empty')); } }, 300); } catch (error) { item.classList.remove('is-removing'); this.button.disabled = false; console.error('Remove failed:', error); } }}
customElements.define('remove-button', RemoveButton);Remove Animation CSS
.cart-item { transition: height 0.3s ease, opacity 0.3s ease, margin 0.3s ease, padding 0.3s ease; overflow: hidden;}
.cart-item.is-removing { pointer-events: none;}
.cart-remove__button { display: flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-sm); font-size: 0.8125rem; color: var(--color-text-light); background: none; border: none; cursor: pointer; transition: color 0.2s;}
.cart-remove__button:hover { color: var(--color-sale);}
.cart-remove__button svg { width: 16px; height: 16px;}Cart Notes
Let customers add order notes:
{# snippets/cart-note.liquid #}
<div class="cart-note"> <label for="cart-note" class="cart-note__label"> Order Notes <span class="cart-note__optional">(optional)</span> </label>
<textarea id="cart-note" name="note" class="cart-note__textarea" rows="3" placeholder="Special instructions for your order..." >{{ cart.note }}</textarea></div>AJAX Note Update
class CartNote extends HTMLElement { connectedCallback() { this.textarea = this.querySelector('textarea'); this.debounceTimer = null;
this.textarea.addEventListener('input', () => this.onInput()); this.textarea.addEventListener('blur', () => this.save()); }
onInput() { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => this.save(), 1000); }
async save() { const note = this.textarea.value;
try { await fetch('/cart/update.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ note }), });
this.showSaved(); } catch (error) { console.error('Failed to save note:', error); } }
showSaved() { const indicator = this.querySelector('.cart-note__saved'); if (indicator) { indicator.hidden = false; setTimeout(() => { indicator.hidden = true; }, 2000); } }}
customElements.define('cart-note', CartNote);<cart-note class="cart-note"> <label for="cart-note">Order Notes</label> <textarea id="cart-note" name="note">{{ cart.note }}</textarea> <span class="cart-note__saved" hidden>Saved!</span></cart-note>Cart Attributes
Store custom data on the cart:
{# Gift wrapping option #}<div class="cart-attribute"> <label class="cart-attribute__checkbox"> <input type="checkbox" name="attributes[gift_wrap]" value="Yes" {% if cart.attributes.gift_wrap == 'Yes' %}checked{% endif %} data-cart-attribute > <span>Add gift wrapping (+$5.00)</span> </label></div>
{# Delivery date #}<div class="cart-attribute"> <label for="delivery-date">Preferred Delivery Date</label> <input type="date" id="delivery-date" name="attributes[delivery_date]" value="{{ cart.attributes.delivery_date }}" data-cart-attribute ></div>Updating Attributes via AJAX
document.querySelectorAll('[data-cart-attribute]').forEach((input) => { input.addEventListener('change', async () => { const name = input.name.match(/attributes\[(.+)\]/)[1]; const value = input.type === 'checkbox' ? (input.checked ? input.value : '') : input.value;
await fetch('/cart/update.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ attributes: { [name]: value }, }), }); });});Displaying Discounts
Line-Level Discounts
{%- for item in cart.items -%} <div class="cart-item"> {# ... item details ... #}
{# Show if discounted #} {%- if item.original_line_price != item.final_line_price -%} <div class="cart-item__pricing"> <span class="cart-item__original-price"> {{ item.original_line_price | money }} </span> <span class="cart-item__final-price"> {{ item.final_line_price | money }} </span> </div>
{# List applied discounts #} {%- if item.line_level_discount_allocations.size > 0 -%} <ul class="cart-item__discounts"> {%- for discount in item.line_level_discount_allocations -%} <li class="cart-item__discount"> <span class="cart-item__discount-title"> {{ discount.discount_application.title }} </span> <span class="cart-item__discount-amount"> -{{ discount.amount | money }} </span> </li> {%- endfor -%} </ul> {%- endif -%} {%- else -%} <span class="cart-item__price"> {{ item.final_line_price | money }} </span> {%- endif -%} </div>{%- endfor -%}Cart-Level Discounts
{# In cart totals #}{%- if cart.cart_level_discount_applications.size > 0 -%} <div class="cart-discounts"> {%- for discount in cart.cart_level_discount_applications -%} <div class="cart-discount"> <span class="cart-discount__title"> {% render 'icon-tag' %} {{ discount.title }} </span> <span class="cart-discount__amount"> -{{ discount.total_allocated_amount | money }} </span> </div> {%- endfor -%} </div>{%- endif -%}Discount Code Input
Let customers apply discount codes:
{# snippets/cart-discount-form.liquid #}
<div class="cart-discount-form"> <label for="discount-code" class="visually-hidden">Discount code</label>
<div class="cart-discount-form__input-group"> <input type="text" id="discount-code" name="discount" placeholder="Discount code" class="cart-discount-form__input" > <button type="submit" class="cart-discount-form__button"> Apply </button> </div>
<p class="cart-discount-form__note"> Discount codes are applied at checkout </p></div>Note: Discount codes are validated at checkout, not in the cart. The cart just passes the code along.
Complete Cart Totals with Discounts
{# snippets/cart-totals.liquid #}
<div class="cart-totals"> {# Subtotal (before discounts) #} <div class="cart-totals__row"> <span>Subtotal</span> <span>{{ cart.items_subtotal_price | money }}</span> </div>
{# Cart-level discounts #} {%- for discount in cart.cart_level_discount_applications -%} <div class="cart-totals__row cart-totals__row--discount"> <span> {% render 'icon-tag' %} {{ discount.title }} </span> <span>-{{ discount.total_allocated_amount | money }}</span> </div> {%- endfor -%}
{# Shipping #} <div class="cart-totals__row"> <span>Shipping</span> <span>Calculated at checkout</span> </div>
{# Total #} <div class="cart-totals__row cart-totals__row--total"> <span>Total</span> <span>{{ cart.total_price | money }}</span> </div>
{# Savings summary #} {%- if cart.total_discount > 0 -%} <p class="cart-totals__savings"> You're saving {{ cart.total_discount | money }}! </p> {%- endif -%}</div>Discount Styles
.cart-item__pricing { display: flex; gap: var(--spacing-sm); align-items: baseline;}
.cart-item__original-price { text-decoration: line-through; color: var(--color-text-light); font-size: 0.875rem;}
.cart-item__final-price { color: var(--color-sale); font-weight: 600;}
.cart-item__discounts { list-style: none; padding: 0; margin: var(--spacing-xs) 0 0;}
.cart-item__discount { display: flex; justify-content: space-between; font-size: 0.8125rem; color: var(--color-sale);}
.cart-discount { display: flex; justify-content: space-between; padding: var(--spacing-sm) 0; color: var(--color-sale);}
.cart-discount__title { display: flex; align-items: center; gap: var(--spacing-xs);}
.cart-discount__title svg { width: 14px; height: 14px;}
.cart-totals__row--discount { color: var(--color-sale);}
.cart-totals__savings { text-align: center; color: var(--color-sale); font-weight: 500; margin: var(--spacing-md) 0 0; padding: var(--spacing-sm); background: rgba(16, 185, 129, 0.1); border-radius: var(--border-radius);}Clear Cart Button
Allow clearing the entire cart:
<button type="button" class="cart-clear" data-clear-cart> Clear Cart</button>document.querySelector('[data-clear-cart]')?.addEventListener('click', async () => { if (!confirm('Remove all items from cart?')) return;
await fetch('/cart/clear.js', { method: 'POST' });
location.reload();});Practice Exercise
Implement cart features including:
- Remove buttons with smooth animation
- Cart note with autosave
- Gift wrap checkbox attribute
- Discount display for line and cart level
- Savings summary
Test scenarios:
- Removing items one by one
- Saving and retrieving cart notes
- Toggling cart attributes
- Viewing with various discount types applied
Key Takeaways
- Remove by setting quantity to 0
- Animate removal for smooth UX
- Debounce note updates to reduce API calls
- Cart attributes persist through checkout
- Show line-level discounts per item
- Show cart-level discounts in totals
- Display total savings to reinforce value
- Discount codes are validated at checkout
What’s Next?
The final cart lesson covers Cross-Sells Inside Cart Drawer for increasing average order value.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...