Shopify Horizon Theme: Mastering main-cart.liquid and cart-items-component
Horizon replaces Dawn's main-cart-items.liquid with a web-component architecture. Learn exactly where cart logic lives, how to customize it safely, and
If you've searched your Horizon theme for main-cart-items.liquid and come up empty, you are not alone. Horizon deliberately does not have that file because it replaced the Dawn-era section model with a native web-component architecture centered on cart-items-component. Understanding where cart logic actually lives, and how to extend it without breaking on the next Shopify update, is the single most important thing you can do before touching Horizon's cart.
Key takeaways
- Horizon has no
main-cart-items.liquidinsections/; the equivalent ismain-cart.liquidplus thecart-items-componentweb component. - The cart component communicates via custom events (
cart:update,CartUpdateEvent) rather than DOM manipulation. - All cart customizations should go into new blocks in
blocks/or new snippets, not edits to core Liquid files. - Horizon's GitHub repo (Shopify/horizon) tracks releases, but as of April 2026 the Theme Store version (v3.5.1) has been ahead of the public repo (v3.4.0), so always diff before pulling upstream.
- App blocks are the upgrade-safe path for injecting third-party cart functionality.
Why Horizon ditched main-cart-items.liquid
Every Dawn-based customization tutorial tells you to open sections/main-cart-items.liquid. That file is the monolithic section that renders the cart table in Dawn and most of its derivatives. Horizon, launched in the Shopify Summer Editions 2025 release on May 21, 2025, takes a completely different approach.
Instead of one large Liquid section, Horizon splits responsibility:
sections/main-cart.liquid, the top-level JSON-template section that owns the page scaffold, color scheme, and{% content_for "blocks" %}rendering.cart-items-component, a native Web Component (a Custom Element registered in JavaScript) that owns all the interactive cart-row logic: quantity updates, remove buttons, line-item properties, and Section Rendering API re-renders.
This is not an accident. Horizon's design principles, as documented in the Shopify/horizon GitHub repository, explicitly call for server-rendered HTML via Liquid with async, on-demand re-rendering done sparingly as a progressive enhancement. The web component layer handles the progressive part; Liquid handles the static render.
The Horizon file structure you actually need to know
Horizon's source tree is meaningfully different from Dawn's. According to community-documented architecture research, the theme ships with:
assets/, 100-plus CSS, JS, and static filesblocks/, 94 theme block Liquid files, reusable across multiple sectionssections/, 41 section filessnippets/, 93 reusable Liquid snippetstemplates/, 14 JSON template files
For cart work, the files that matter are:
| File / Component | Role |
|---|---|
sections/main-cart.liquid | Page scaffold, color scheme, block rendering |
cart-items-component (JS) | Interactive cart rows, quantity, remove |
cart-drawer-component (JS) | Slide-out drawer, discount code support |
assets/cart-discount.js | Discount code handling for the drawer |
snippets/cart-line-item.liquid | Renders a single line item row (the file to study for layout customization) |
The main-cart.liquid section itself is lean by design. It renders the cart page skeleton and delegates block rendering via {% content_for "blocks" %}. The heavy lifting of displaying and updating line items belongs to cart-items-component.
How cart-items-component actually works
Horizon uses a custom web component architecture built entirely on native Web Components APIs, no React, no Vue, no jQuery. Every interactive component, including cart-items-component, extends a base Component class that handles automatic DOM reference management and declarative event handling.
All Horizon web components follow the standard Custom Elements lifecycle:
connectedCallback(), fires when the component is inserted into the DOM (the initial server render)disconnectedCallback(), fires when it is removedupdatedCallback(), fires when the component is re-rendered by the Section Rendering API after a cart mutation
For cross-component communication, Horizon uses a ThemeEvents utility (found in events.js) and a typed CartUpdateEvent. When a quantity changes or an item is removed, the component dispatches a cart:update event that other components, the cart drawer, the cart icon badge, any custom sections, can listen for:
```javascript import { ThemeEvents, CartUpdateEvent } from '@theme/events';
document.addEventListener(ThemeEvents.cartUpdate, (event) => { console.log('Cart updated:', event.detail); }); ```
This event-driven pattern is the correct extension point. If you are building a custom "Frequently Bought Together" section or any feature that adds items programmatically, dispatch a properly structured cart:update event and the cart drawer will open and refresh automatically. You do not need to manipulate cart-items-component's DOM directly.
The main-cart-items.liquid confusion, explained
Shopify's own Help Center documentation for adding a date picker or custom cart fields still points merchants to main-cart-items.liquid. That works fine on Dawn. On Horizon 3.3.0 and later, that file does not exist in sections/, which is exactly the error reported by Horizon users as recently as February 2026.
Here is the correct mapping:
Dawn (OS 2.0 pattern): Custom cart fields go inside sections/main-cart-items.liquid, wrapping them in the existing cart form.
Horizon pattern: Custom cart fields belong in one of three places, in order of preference:
- A new block file (
blocks/my-cart-field.liquid) with{% schema %}that exposes merchant-configurable settings. Add it to the cart template JSON so merchants can toggle it in the editor. - A snippet (
snippets/my-cart-field.liquid) rendered conditionally from insidemain-cart.liquidor from a block. Good for logic that must always appear. - A direct edit to
main-cart.liquidas a last resort, with a clear comment noting exactly what changed and why, because Horizon receives regular updates and any change to a core file must be manually re-applied after each update.
For capturing a cart attribute (like a delivery date or gift message), the Shopify Liquid docs confirm the input must carry name="attributes[attribute-name]" and form="cart". In Horizon, the cart form ID is still cart, so the HTML itself is identical to Dawn, only the file you edit has changed.
Triggering cart events from custom code
If your custom section adds items to the cart via the AJAX Cart API (/cart/add.js) rather than through a standard Shopify product form, you need to manually fire the right event so Horizon's components respond. The minimal working pattern, confirmed in the Shopify Developer Community forums:
``javascript document.dispatchEvent( new CustomEvent('cart:update', { bubbles: true, detail: { data: { itemCount: cart.item_count, source: 'my-custom-component' } } }) ); ``
Alternatively, if your feature uses a standard {% form 'product', product %} Liquid tag, wrapping it inside Horizon's existing web component structure gives you automatic AJAX submission and cart drawer opening at no extra JavaScript cost.
Update safety: the GitHub repo lag problem
One practical issue worth knowing: as of April 2026, the Horizon theme on the Theme Store was at v3.5.1 while the public Shopify/horizon GitHub repository was stuck at v3.4.0 for roughly two months. This means if you use the repo as an upstream remote to pull Horizon changes into your customized fork, you will miss recent fixes.
The safe workflow:
- Download the latest theme zip directly from the Theme Store.
- Use a diff tool to compare the downloaded
main-cart.liquidand any affected snippets against your forked version. - Re-apply your customizations on top of the new baseline.
- Never edit
main-cart.liquidor any core Liquid file inline unless you have documented every change and will own the merge process on every future update.
Horizon's {% stylesheet %} and {% javascript %} tag support inside blocks and snippets (a Liquid Storefronts feature highlighted since the Summer 2025 launch) means your custom block's CSS and JS are bundled and cached per-component. Use this rather than injecting styles through theme.liquid.
Customizing cart line items the upgrade-safe way
If your goal is to change what is rendered for each cart row (custom badges, line item property display, bundle indicators), study snippets/cart-line-item.liquid rather than hunting for main-cart-items.liquid. This snippet is what cart-items-component iterates over for each line_item. Your options:
- Override via a block: Create a
blocks/cart-row-badge.liquidthat renders additional markup. Inject it through the cart template JSON. - Use app blocks: If a third-party app needs to inject UI into cart rows, app blocks are the sanctioned path. Horizon's schema supports
appblock types, and this approach survives theme updates cleanly. - Line item properties display: Loop through
item.propertiesinside a snippet or block rather than inside the core section. The Shopify Liquid docs confirm line items with unique properties are split into separate line items, so your display logic needs to handle that case.
For a deeper look at how Horizon's block nesting (up to 8 levels deep, compared to Dawn's 2) changes the architecture of complex customizations, see my Shopify theme development guide. If cart page performance is a concern alongside customization, the Shopify speed optimization breakdown covers how Horizon's component-scoped CSS and JS delivery affects Core Web Vitals scores.
Summary
Horizon's cart is not broken, it is redesigned. The main-cart-items.liquid file from Dawn has been replaced by main-cart.liquid (the scaffold) plus cart-items-component (the interactive layer). Customize via new blocks and snippets, communicate via the cart:update event system, and treat any edit to a core Liquid file as technical debt you will repay on the next update. That discipline is what separates a Horizon store that scales from one that turns into a merge nightmare six months in.
Frequently asked questions
Where is main-cart-items.liquid in the Shopify Horizon theme?
Horizon does not have a main-cart-items.liquid file. The cart page scaffold is in sections/main-cart.liquid, and the interactive cart row logic is handled by the cart-items-component web component. To add custom cart fields, create a new block in the blocks/ directory or add a snippet rendered from main-cart.liquid.
How do I add custom cart attributes or a date picker to the Horizon theme?
Add an HTML input with name="attributes[your-attribute-name]" and form="cart" to a new block file or snippet, then reference that block in your cart template JSON. Do not edit main-cart.liquid directly unless necessary, because Horizon receives regular updates that would overwrite your changes.
How do I open the Horizon cart drawer programmatically after adding items via JavaScript?
Dispatch a custom cart:update event on the document with bubbles set to true and a detail object containing itemCount and source. Horizon's cart-drawer-component listens for this event and will open and refresh automatically. If you use a standard Shopify product form wrapped in Horizon's web component structure, the drawer opens without any extra JavaScript.