Overview

CartOS themes use a Liquid-based template engine similar to Shopify's Online Store 2.0, but with important differences in syntax and conventions. If you've built Shopify themes before, you'll feel right at home — just pay close attention to the CartOS-specific conventions documented below.

Themes can be published to the CartOS Theme Store (accessible at themes.cart-os.com) or built for individual merchants. All themes connect to a GitHub repository for version-controlled development.

🛈
Naming Convention: Theme files use the .html extension (not .liquid). CartOS processes all .html files in theme directories through the Liquid rendering engine.

Directory Structure

Every CartOS theme follows this standard directory structure:

theme-handle/ ├── config/ │ ├── theme.json # Theme metadata & settings schema │ └── settings_data.json # Default settings values ├── layout/ │ └── theme.html # Main layout wrapper ├── templates/ │ ├── index.html # Homepage │ ├── collection.html # Collection/category page │ ├── product.html # Product detail page │ ├── cart.html # Cart page │ ├── checkout.html # Checkout (required) │ ├── thank-you.html # Order confirmation (required) │ ├── page.html # Generic CMS page │ ├── contact.html # Contact page │ ├── blog.html # Blog listing │ ├── article.html # Blog article │ ├── search.html # Search results │ ├── 404.html # Not found │ └── customers/ │ ├── login.html │ ├── register.html │ ├── account.html │ ├── order.html │ ├── addresses.html │ └── reset_password.html ├── sections/ # Reusable page sections ├── snippets/ # Includable partial templates ├── assets/ │ ├── css/ │ ├── js/ │ └── images/ └── locales/ └── en.json # English translations

Layout File

The layout/theme.html file wraps every page. It must include {{ content_for_layout }} where the page template output should appear.

layout/theme.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ page_title }} | {{ shop.name }}</title>
    {{ content_for_header }}
    {% bolt_hook 'head' %}
</head>
<body>
    {% include 'header' %}
    {{ content_for_layout }}
    {% include 'footer' %}
    {% bolt_hook 'body_end' %}
</body>
</html>

Liquid Basics

Liquid uses two delimiters. Output tags {{ }} display data on the page, and Logic tags {% %} execute logic without outputting directly:

<!-- Output: displays the product title -->
{{ product.title }}

<!-- Logic: conditional rendering -->
{% if product.available %}
  <span>In Stock</span>
{% else %}
  <span>Sold Out</span>
{% endif %}

<!-- Loops -->
{% for product in collection.products %}
  <h3>{{ product.title }}</h3>
{% endfor %}

<!-- Filters -->
{{ product.price | money }}
{{ product.title | upcase }}
{{ 'now' | date: '%Y-%m-%d' }}

CartOS vs Shopify Differences

These differences are mandatory. Using Shopify conventions instead will cause rendering failures.

⚠️
Critical: CartOS does NOT support {%- -%} whitespace control tags or multi-line {%- liquid -%} blocks. Use individual single-line Liquid tags only.
FeatureShopify (wrong)CartOS (correct)
Including snippets {% render 'name' %} {% include 'name' %}
Bolt hook points {% hook 'name' %} {% bolt_hook 'name' %}
Settings access section.settings.heading settings.hero.heading
Navigation menus linklists['main-menu'] navigation.header / navigation.footer
Cart item title item.product.title item.title
Cart item price item.product.price item.price
Whitespace control {%- -%} Not supported — use {% %}
Multi-line liquid {%- liquid ... -%} Not supported — use separate tags
Placeholder filter | placeholder Not supported — use placeholder SVGs
File extension .liquid .html

CartOS provides two built-in navigation objects: navigation.header and navigation.footer. Each contains an array of link objects with title, url, and optional nested links for dropdowns.

sections/header.html
<nav>
  {% for link in navigation.header %}
    {% if link.links.size > 0 %}
      <div class="dropdown">
        <a href="{{ link.url }}">{{ link.title }}</a>
        <div class="dropdown-menu">
          {% for child in link.links %}
            <a href="{{ child.url }}">{{ child.title }}</a>
          {% endfor %}
        </div>
      </div>
    {% else %}
      <a href="{{ link.url }}">{{ link.title }}</a>
    {% endif %}
  {% endfor %}
</nav>

Images

Product and collection images use the image_url filter with a width parameter. Cart item images are pre-formatted URLs and do not need this filter.

<!-- Product images: ALWAYS use image_url filter -->
<img src="{{ product.featured_image | image_url: width: 600 }}">
<img src="{{ collection.image | image_url: width: 400 }}">

<!-- Cart item images: already formatted, use directly -->
<img src="{{ item.image }}">

Cart JavaScript API

CartOS provides a JSON-based cart API for add-to-cart, quantity changes, and cart retrieval. All requests use JSON content type.

JavaScript
// Add to cart
fetch('/cart/add.json', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ id: variantId, quantity: 1 })
});

// Update quantity (set quantity: 0 to remove)
fetch('/cart/change.json', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ id: lineItemId, quantity: newQuantity })
});

// Get cart contents
fetch('/cart.json');

Cart Drawer Reopening Pattern

After cart changes, redirect with ?cart=open to reopen the cart drawer:

// After a cart update, reopen the drawer
const url = new URL(window.location.href);
url.searchParams.set('cart', 'open');
window.location.href = url.toString();

// Handle on page load
document.addEventListener('DOMContentLoaded', function() {
  const params = new URLSearchParams(window.location.search);
  if (params.get('cart') === 'open') {
    params.delete('cart');
    const newUrl = params.toString()
      ? window.location.pathname + '?' + params
      : window.location.pathname;
    window.history.replaceState({}, '', newUrl);
    openCartDrawer();
  }
});

Theme Settings

Theme settings are defined in config/theme.json as a schema. Default values are stored in config/settings_data.json. Merchants customize these in the admin under Online Store › Themes › Customize.

config/theme.json (excerpt)
{
  "name": "My Theme",
  "version": "1.0.0",
  "settings": {
    "colors": {
      "label": "Colors",
      "settings": [
        { "id": "color_primary", "type": "color", "label": "Primary", "default": "#1a5fb4" },
        { "id": "color_background", "type": "color", "label": "Background", "default": "#ffffff" }
      ]
    },
    "hero": {
      "label": "Hero Section",
      "settings": [
        { "id": "enabled", "type": "checkbox", "label": "Show hero", "default": true },
        { "id": "heading", "type": "text", "label": "Heading", "default": "Welcome" },
        { "id": "image", "type": "image_picker", "label": "Hero image" }
      ]
    }
  }
}

Accessing Settings

Settings from theme.json are stored in settings_data.json and accessed via the global settings object. Never use section.settings — it is not populated by CartOS.

<!-- WRONG: section.settings is NOT populated -->
{% if section.settings.enabled %}  <!-- Will always be empty! -->

<!-- CORRECT: use the global settings object -->
{% if settings.hero.enabled %}
  <h1>{{ settings.hero.heading }}</h1>
{% endif %}

<!-- Image picker settings use a flat key -->
<img src="{{ settings.hero_image }}">

<!-- Use fallbacks for robustness -->
{% assign hero_heading = settings.hero.heading | default: settings.hero_heading %}

Bolt Hook Points

Themes must include {% bolt_hook %} tags at specific locations so that installed bolts can inject their content. These are required for Theme Store approval.

Hook NameLocationCommon Use
headInside <head>CSS, meta tags, analytics
body_endBefore </body>JavaScript, chat widgets
cart_before_itemsBefore cart item loopCart messaging, announcements
cart_after_itemsAfter cart item loopUpsells, shipping protection
cart_before_checkoutBefore checkout buttonFinal offers, discount codes
product_metaAfter product priceBadges, ratings, trust seals
product_tabsIn product description areaReviews, spec sheets, FAQs
<!-- Example: cart.html with bolt hooks -->
{% bolt_hook 'cart_before_items' %}

{% for item in cart.items %}
  {% unless item.properties._bolt %}
    {% include 'cart-item', item: item %}
  {% endunless %}
{% endfor %}

{% bolt_hook 'cart_after_items' %}
{% bolt_hook 'cart_before_checkout' %}

Cart Item Filtering

Bolts can add hidden items to the cart (e.g., shipping protection, warranty). These items have a _bolt property. Always filter them out in your cart display loop to avoid confusing customers:

{% for item in cart.items %}
  {% unless item.properties._bolt %}
    <!-- Render visible cart item -->
    <div data-line-id="{{ item.id }}">
      <img src="{{ item.image }}">
      <h4>{{ item.title }}</h4>
      <span>{{ item.price | money }}</span>
    </div>
  {% endunless %}
{% endfor %}

CartOS search uses a products variable on the search results page. The search term is not available in Liquid — use JavaScript to extract it from the URL:

<!-- templates/search.html -->
<h1>Search Results</h1>
<p>Results for: <span id="searchTerm"></span></p>

{% for product in products %}
  {% include 'product-card', product: product %}
{% endfor %}

<script>
  const params = new URLSearchParams(window.location.search);
  document.getElementById('searchTerm').textContent = params.get('q') || '';
</script>

Publishing to the Theme Store

To publish a theme to the CartOS Theme Store:

  1. Ensure your theme includes all required templates (especially checkout.html and thank-you.html).
  2. Include all required {% bolt_hook %} points in your layout and templates.
  3. Filter bolt items in cart loops with {% unless item.properties._bolt %}.
  4. Connect your GitHub repository to your developer account.
  5. Submit for review through the developer portal — our team will test compatibility and provide feedback.
Tip: Study the official Dawn theme as a reference implementation. It demonstrates all CartOS conventions correctly and is available in the Theme Store.