Cart Page and Cart Drawer Beginner 8 min read

Empty Cart State and Conditional Rendering

Design engaging empty cart experiences with calls-to-action, product suggestions, and smooth transitions between empty and filled states.

An empty cart is an opportunity, not a dead end. A well-designed empty state guides customers back to shopping while maintaining engagement.

Basic Empty Cart Check

{%- if cart.item_count == 0 -%}
{# Empty cart content #}
{%- else -%}
{# Cart items #}
{%- endif -%}

Alternative check:

{%- if cart.items.size == 0 -%}
{# Empty cart #}
{%- endif -%}

Simple Empty Cart State

{# snippets/cart-empty.liquid #}
<div class="cart-empty">
<div class="cart-empty__icon">
{% render 'icon-cart' %}
</div>
<h2 class="cart-empty__title">Your cart is empty</h2>
<p class="cart-empty__message">
Looks like you haven't added anything to your cart yet.
</p>
<a href="{{ routes.all_products_collection_url }}" class="button button--primary">
Continue Shopping
</a>
</div>

Empty Cart Styles

.cart-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: var(--spacing-2xl) var(--spacing-md);
min-height: 300px;
}
.cart-empty__icon {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
margin-bottom: var(--spacing-lg);
background: var(--color-background-secondary);
border-radius: 50%;
}
.cart-empty__icon svg {
width: 40px;
height: 40px;
color: var(--color-text-light);
}
.cart-empty__title {
font-size: 1.5rem;
margin: 0 0 var(--spacing-sm);
}
.cart-empty__message {
color: var(--color-text-light);
margin: 0 0 var(--spacing-lg);
max-width: 400px;
}

Empty Cart with Suggestions

Keep customers engaged with product recommendations:

{# snippets/cart-empty.liquid #}
<div class="cart-empty">
<div class="cart-empty__content">
<div class="cart-empty__icon">
{% render 'icon-cart' %}
</div>
<h2 class="cart-empty__title">Your cart is empty</h2>
<p class="cart-empty__message">
Browse our collection and find something you'll love.
</p>
<a href="{{ routes.all_products_collection_url }}" class="button button--primary">
Start Shopping
</a>
</div>
{# Suggested products #}
{%- assign featured = collections['bestsellers'] | default: collections['all'] -%}
{%- if featured.products.size > 0 -%}
<div class="cart-empty__suggestions">
<h3 class="cart-empty__suggestions-title">Popular Products</h3>
<ul class="cart-empty__products">
{%- for product in featured.products limit: 4 -%}
<li class="cart-empty__product">
{% render 'product-card', product: product, show_quick_add: true %}
</li>
{%- endfor -%}
</ul>
</div>
{%- endif -%}
</div>

Empty Cart Drawer

For the slide-out cart drawer:

{# snippets/cart-drawer-empty.liquid #}
<div class="cart-drawer-empty">
<div class="cart-drawer-empty__icon">
{% render 'icon-shopping-bag' %}
</div>
<p class="cart-drawer-empty__title">Your cart is empty</p>
<p class="cart-drawer-empty__message">
Add items to get started
</p>
<button class="button button--secondary" data-cart-close>
Continue Shopping
</button>
</div>
.cart-drawer-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
height: 100%;
padding: var(--spacing-xl);
}
.cart-drawer-empty__icon {
width: 64px;
height: 64px;
margin-bottom: var(--spacing-md);
opacity: 0.4;
}
.cart-drawer-empty__title {
font-size: 1.125rem;
font-weight: 600;
margin: 0 0 var(--spacing-xs);
}
.cart-drawer-empty__message {
color: var(--color-text-light);
margin: 0 0 var(--spacing-lg);
}

Recently Viewed in Empty Cart

Show products they’ve already looked at:

<div class="cart-empty__recently-viewed" data-recently-viewed>
<h3>Recently Viewed</h3>
<div class="cart-empty__recently-viewed-products" data-recently-viewed-products>
{# Populated via JavaScript #}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('[data-recently-viewed-products]');
if (!container) return;
const viewed = JSON.parse(localStorage.getItem('recently_viewed') || '[]');
if (viewed.length === 0) {
container.closest('[data-recently-viewed]').hidden = true;
return;
}
// Fetch and display recently viewed products
Promise.all(viewed.slice(0, 4).map(handle =>
fetch(`/products/${handle}?view=card`)
.then(r => r.text())
.catch(() => null)
)).then(cards => {
container.innerHTML = cards.filter(Boolean).join('');
});
});
</script>

Conditional Template Sections

Use different sections for empty vs filled:

{# templates/cart.liquid or sections/cart-main.liquid #}
{%- if cart.item_count > 0 -%}
<div class="cart-page cart-page--has-items">
{# Full cart layout #}
<div class="cart-page__items">
{% render 'cart-items' %}
</div>
<div class="cart-page__sidebar">
{% render 'cart-totals' %}
</div>
</div>
{%- else -%}
<div class="cart-page cart-page--empty">
{% render 'cart-empty' %}
</div>
{%- endif -%}

Animation Transitions

Smooth transitions when cart empties:

.cart-items {
transition: opacity 0.3s ease;
}
.cart-items.is-updating {
opacity: 0.5;
pointer-events: none;
}
.cart-empty {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

JavaScript Empty State Handling

Update UI when last item is removed:

class CartItems extends HTMLElement {
async removeItem(key) {
this.classList.add('is-updating');
try {
const response = await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: key, quantity: 0 })
});
const cart = await response.json();
if (cart.item_count === 0) {
// Show empty state
this.showEmptyState();
} else {
// Refresh cart content
this.refresh();
}
} catch (error) {
console.error('Remove failed:', error);
} finally {
this.classList.remove('is-updating');
}
}
showEmptyState() {
const cartPage = document.querySelector('.cart-page');
// Fetch empty state HTML
fetch('/?section_id=cart-empty')
.then(r => r.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const emptyState = doc.querySelector('.cart-empty');
if (emptyState) {
cartPage.innerHTML = '';
cartPage.appendChild(emptyState);
cartPage.classList.remove('cart-page--has-items');
cartPage.classList.add('cart-page--empty');
}
});
// Update header cart count
document.querySelectorAll('[data-cart-count]').forEach(el => {
el.textContent = '0';
el.hidden = true;
});
}
}

Multiple Call-to-Action Options

Provide different paths back to shopping:

<div class="cart-empty">
<h2 class="cart-empty__title">Your cart is empty</h2>
<div class="cart-empty__actions">
<a href="{{ routes.collections_url }}" class="button button--primary">
Browse Collections
</a>
{%- if customer -%}
<a href="{{ routes.account_url }}" class="button button--secondary">
View Past Orders
</a>
{%- endif -%}
</div>
{# Quick links #}
<div class="cart-empty__links">
<p>Quick links:</p>
<ul>
<li><a href="{{ collections['new-arrivals'].url }}">New Arrivals</a></li>
<li><a href="{{ collections['bestsellers'].url }}">Best Sellers</a></li>
<li><a href="{{ collections['sale'].url }}">Sale Items</a></li>
</ul>
</div>
</div>

Schema Settings for Empty State

Let merchants customize the empty cart:

{% schema %}
{
"name": "Cart Page",
"settings": [
{
"type": "header",
"content": "Empty cart"
},
{
"type": "text",
"id": "empty_title",
"label": "Empty cart title",
"default": "Your cart is empty"
},
{
"type": "textarea",
"id": "empty_message",
"label": "Empty cart message",
"default": "Looks like you haven't added anything yet."
},
{
"type": "text",
"id": "empty_button_text",
"label": "Button text",
"default": "Continue Shopping"
},
{
"type": "url",
"id": "empty_button_link",
"label": "Button link"
},
{
"type": "collection",
"id": "empty_collection",
"label": "Suggested products collection"
},
{
"type": "range",
"id": "empty_products_count",
"label": "Number of suggested products",
"min": 0,
"max": 8,
"step": 1,
"default": 4
}
]
}
{% endschema %}
{%- if cart.item_count == 0 -%}
<div class="cart-empty">
<h2>{{ section.settings.empty_title }}</h2>
<p>{{ section.settings.empty_message }}</p>
<a href="{{ section.settings.empty_button_link | default: routes.all_products_collection_url }}" class="button">
{{ section.settings.empty_button_text }}
</a>
{%- if section.settings.empty_collection and section.settings.empty_products_count > 0 -%}
<div class="cart-empty__suggestions">
{%- for product in section.settings.empty_collection.products limit: section.settings.empty_products_count -%}
{% render 'product-card', product: product %}
{%- endfor -%}
</div>
{%- endif -%}
</div>
{%- endif -%}

Practice Exercise

Create an empty cart state that:

  1. Shows a friendly message with icon
  2. Has a “Continue Shopping” button
  3. Displays 4 suggested products
  4. Includes quick links to popular collections
  5. Animates in smoothly when cart empties

Test by:

  • Visiting cart page with empty cart
  • Removing last item from cart
  • Checking mobile layout
  • Verifying suggested products load

Key Takeaways

  1. Always check cart.item_count or cart.items.size
  2. Provide clear CTAs to continue shopping
  3. Suggest products to keep customers engaged
  4. Show recently viewed for personalization
  5. Animate transitions between states
  6. Handle dynamically when last item removed
  7. Make it merchant-configurable via schema
  8. Consider mobile layout differences

What’s Next?

The next lesson covers Updating Quantity: Server and AJAX Approach for changing cart quantities.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...