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:
- Shows a friendly message with icon
- Has a “Continue Shopping” button
- Displays 4 suggested products
- Includes quick links to popular collections
- 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
- Always check
cart.item_countorcart.items.size - Provide clear CTAs to continue shopping
- Suggest products to keep customers engaged
- Show recently viewed for personalization
- Animate transitions between states
- Handle dynamically when last item removed
- Make it merchant-configurable via schema
- 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...