Cart Page and Cart Drawer Intermediate 10 min read

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:

  1. Remove buttons with smooth animation
  2. Cart note with autosave
  3. Gift wrap checkbox attribute
  4. Discount display for line and cart level
  5. 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

  1. Remove by setting quantity to 0
  2. Animate removal for smooth UX
  3. Debounce note updates to reduce API calls
  4. Cart attributes persist through checkout
  5. Show line-level discounts per item
  6. Show cart-level discounts in totals
  7. Display total savings to reinforce value
  8. 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...