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
Theme Blocks
App Blocks
Content Blocks
Layout Blocks
Access to all theme blocks like product cards, collection lists, etc. Third-party app integrations (upsells, recommendations, etc.)
text - Rich text content
icon - Icon with optional text
image - Custom images
button - Call-to-action buttons
video - Embedded videos
group - Group multiple blocks
spacer - Add vertical spacing
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
750px - 1399px
- Two-column layout
- Summary sidebar (50vw max)
- Side-by-side items and summary
≥ 1400px
- Two-column layout
- Summary sidebar (fixed width)
- Maximum readability
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
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:
-
Subtotal Display
<div class="cart-summary__subtotal">
<span>{{ 'cart.subtotal' | t }}</span>
<span>{{ cart.total_price | money }}</span>
</div>
-
Shipping Calculator (optional)
{% if settings.enable_shipping_calculator %}
<div class="cart-summary__shipping">
{% render 'shipping-calculator' %}
</div>
{% endif %}
-
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>
-
Total Price
<div class="cart-summary__total">
<span>{{ 'cart.total' | t }}</span>
<strong>{{ cart.total_price | money }}</strong>
</div>
-
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'] }}">