Skip to main content
The Horizon cart provides a responsive, feature-rich shopping cart experience with real-time updates, summary sidebar, and customizable blocks.

Key Features

  • Dynamic Updates: Real-time cart updates without page reload
  • Responsive Layout: Two-column desktop, single-column mobile
  • Cart Summary Sidebar: Sticky summary with totals and checkout button
  • Empty State: Custom empty cart display
  • View Transitions: Smooth animations for cart changes
  • Flexible Blocks: Add custom content blocks to cart page

Cart Structure

The cart uses a component-based architecture:
<cart-items-component
  class="cart-items-component"
  data-section-id="{{ section.id }}"
>
  <!-- Empty cart template -->
  <template id="empty-cart-template">
    <!-- Rendered when cart is empty -->
  </template>
  
  <!-- Main cart display -->
  <div class="cart-page">
    <div class="cart-page__title">...</div>
    <div class="cart-page__items">...</div>
    <div class="cart-page__summary">...</div>
  </div>
</cart-items-component>

Cart Components

Cart Title

<div class="cart-page__title">
  {% content_for 'block',
    id: 'cart-page-title',
    type: '_cart-title'
  %}
</div>
Displays the cart heading (e.g., “Your Cart”).

Cart Items

<div class="cart-page__items">
  {% content_for 'block',
    id: 'cart-page-items',
    type: '_cart-products'
  %}
</div>
Features:
  • Product thumbnails
  • Variant information
  • Quantity selectors
  • Remove buttons
  • Line item properties
  • Real-time price updates

Cart Summary

{% unless cart.empty? %}
  <div class="cart-page__summary">
    {% content_for 'block',
      id: 'cart-page-summary',
      type: '_cart-summary'
    %}
  </div>
{% endunless %}
Includes:
  • Subtotal
  • Shipping estimate
  • Tax information
  • Discount codes
  • Total price
  • Checkout button
  • Payment icons (optional)

Additional Blocks

<div class="cart-page__more-blocks">
  {% content_for 'blocks' %}
</div>
Supports custom blocks:
  • Text
  • Images
  • Icons
  • Buttons
  • Videos
  • Groups
  • Spacers

Layout System

Desktop Layout

Two-column layout with sidebar on medium+ screens:
@media screen and (min-width: 750px) {
  .cart-page {
    display: grid;
    grid-template-columns: 1fr min(50vw, var(--sidebar-width));
    grid-template-rows: min-content min-content 1fr;
    gap: 0 var(--padding-5xl);
  }
  
  .cart-page__summary {
    grid-column: 2;
    grid-row: 1 / -1;
    align-self: stretch;
    grid-template-rows: subgrid;
  }
}
Grid Areas:
  • Column 1: Title + Items
  • Column 2: Summary (spans all rows)

Mobile Layout

Single-column stacked layout:
.cart-page {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
}

.cart-page__summary {
  padding-top: var(--padding-xl);
}

Extended Summary

For page-width layouts, summary can extend to viewport edge:
@media screen and (min-width: 750px) {
  .section--page-width .cart-page:has(.cart-summary--extend) {
    grid-column: 2 / 4;
    grid-template-columns: 1fr minmax(
      var(--sidebar-width),
      calc((100vw - var(--page-width)) / 2 + var(--sidebar-width))
    );
  }
}
This creates a bleed effect where the summary extends to the edge of the viewport while content stays within page width.

Empty Cart State

Custom template for when cart is empty:
<template id="empty-cart-template">
  <div class="cart-page spacing-style cart-page--empty">
    <div class="cart-page__title">
      {% content_for 'block',
        id: 'cart-page-title',
        type: '_cart-title',
        force_empty: true
      %}
    </div>
    
    <div class="cart-page__items">
      {% content_for 'block',
        id: 'cart-page-items',
        type: '_cart-products',
        force_empty: true
      %}
    </div>
    
    {{ cart_page_more_blocks_content }}
  </div>
</template>
Empty State Styling:
.cart-page--empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.cart-page--empty .cart-page__title,
.cart-page--empty .cart-page__more-blocks {
  margin-top: var(--margin-6xl);
}

.cart-page--empty .cart-title {
  text-align: center;
}

Dynamic Cart Updates

The cart automatically updates without page reload:

Quantity Changes

// Quantity selectors trigger cart updates
<input type="number" 
       name="updates[]" 
       value="{{ item.quantity }}"
       min="0"
       data-line="{{ forloop.index }}">

Remove Items

<button type="button"
        data-line="{{ forloop.index }}"
        aria-label="{{ 'cart.remove' | t }}">
  Remove
</button>

Cart API

Uses Shopify Cart API for updates:
// Update quantity
fetch('/cart/update.js', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    updates: { [variantId]: quantity }
  })
});

// Remove item
fetch('/cart/change.js', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    line: lineNumber,
    quantity: 0
  })
});

View Transitions

Smooth animations when cart becomes empty:
html:active-view-transition-type(empty-cart-page) {
  .cart-items-component {
    view-transition-name: cart-page-content;
  }
}

::view-transition-old(cart-page-content) {
  animation: cart-page-content-old var(--animation-speed-fast) 
             var(--animation-easing) forwards;
}

@keyframes cart-page-content-old {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
    filter: blur(4px);
  }
}

Supported Block Types

{
  "type": "@theme"
}
Access to all theme blocks like product cards, collection lists, etc.

Settings

Section Width

section_width:
  options:
    - page-width (default)
    - full-width
  default: page-width

Color Scheme

color_scheme:
  type: color_scheme
  default: scheme-1

Padding

padding-block-start:
  min: 0
  max: 100
  step: 1
  unit: px
  default: 0

Spacing Between Sections

.cart-page__title + .cart-page__items {
  margin-block-start: var(--margin-lg);
}

.cart-page__summary {
  padding-top: var(--padding-xl);
}

@media screen and (min-width: 750px) {
  .cart-page__summary {
    padding-top: 0;
  }
}

Responsive Breakpoints

< 750px
  • Single column layout
  • Summary below items
  • Full-width elements
  • Touch-optimized quantity selectors

Performance Optimizations

Debounced Updates

Quantity changes are debounced to prevent excessive API calls:
let updateTimeout;
function updateQuantity(line, quantity) {
  clearTimeout(updateTimeout);
  updateTimeout = setTimeout(() => {
    // Update cart via API
  }, 300);
}

Section Rendering

Cart uses section rendering for partial updates:
fetch(`${window.location.pathname}?section_id=${sectionId}`)
  .then(response => response.text())
  .then(html => {
    // Update only cart section, not entire page
  });

Accessibility

<div role="region" aria-label="{{ 'cart.title' | t }}">
  <table role="table" aria-label="{{ 'cart.items' | t }}">
    <tr role="row">
      <td role="cell">...</td>
    </tr>
  </table>
</div>
<div role="status" aria-live="polite" aria-atomic="true">
  {{ 'cart.item_updated' | t }}
</div>
Announces cart updates to screen readers.
  • Tab through all interactive elements
  • Enter/Space to activate buttons
  • Arrow keys for quantity steppers
  • Focus visible on all controls
<label for="quantity-{{ item.key }}">
  {{ 'cart.quantity' | t }}
</label>
<input id="quantity-{{ item.key }}" type="number">

Schema Configuration

{
  "name": "Cart",
  "disabled_on": {
    "groups": ["header", "footer"]
  },
  "blocks": [
    { "type": "@theme" },
    { "type": "@app" },
    { "type": "text" },
    { "type": "icon" },
    { "type": "image" },
    { "type": "button" },
    { "type": "video" },
    { "type": "group" },
    { "type": "spacer" }
  ],
  "settings": [
    {
      "type": "select",
      "id": "section_width",
      "label": "Width",
      "options": [
        { "value": "page-width", "label": "Page" },
        { "value": "full-width", "label": "Full" }
      ],
      "default": "page-width"
    },
    {
      "type": "color_scheme",
      "id": "color_scheme",
      "label": "Color scheme",
      "default": "scheme-1"
    },
    {
      "type": "range",
      "id": "padding-block-start",
      "label": "Top padding",
      "min": 0,
      "max": 100,
      "step": 1,
      "unit": "px",
      "default": 0
    },
    {
      "type": "range",
      "id": "padding-block-end",
      "label": "Bottom padding",
      "min": 0,
      "max": 100,
      "step": 1,
      "unit": "px",
      "default": 0
    }
  ]
}

Cart Summary Blocks

The _cart-summary block renders:
  1. Subtotal Display
    <div class="cart-summary__subtotal">
      <span>{{ 'cart.subtotal' | t }}</span>
      <span>{{ cart.total_price | money }}</span>
    </div>
    
  2. Shipping Calculator (optional)
    {% if settings.enable_shipping_calculator %}
      <div class="cart-summary__shipping">
        {% render 'shipping-calculator' %}
      </div>
    {% endif %}
    
  3. Discount Code Input
    <form class="cart-summary__discount">
      <input type="text" name="discount" placeholder="{{ 'cart.discount_code' | t }}">
      <button type="submit">{{ 'cart.apply' | t }}</button>
    </form>
    
  4. Total Price
    <div class="cart-summary__total">
      <span>{{ 'cart.total' | t }}</span>
      <strong>{{ cart.total_price | money }}</strong>
    </div>
    
  5. Checkout Button
    <button type="submit" name="checkout" class="button button--primary">
      {{ 'cart.checkout' | t }}
    </button>
    

Notes API Integration

Cart supports order notes:
<div class="cart-summary__note">
  <label for="cart-note">{{ 'cart.note' | t }}</label>
  <textarea id="cart-note" name="note">{{ cart.note }}</textarea>
</div>

Attributes API

Support for cart attributes:
<input type="text" 
       name="attributes[Gift message]" 
       value="{{ cart.attributes['Gift message'] }}">

Build docs developers (and LLMs) love