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.
.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:
Layout File
The layout/theme.html file wraps every page. It must include {{ content_for_layout }} where the page template output should appear.
<!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.
{%- -%} whitespace control tags or multi-line {%- liquid -%} blocks. Use individual single-line Liquid tags only.| Feature | Shopify (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 |
Navigation
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.
<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.
// 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.
{
"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 Name | Location | Common Use |
|---|---|---|
head | Inside <head> | CSS, meta tags, analytics |
body_end | Before </body> | JavaScript, chat widgets |
cart_before_items | Before cart item loop | Cart messaging, announcements |
cart_after_items | After cart item loop | Upsells, shipping protection |
cart_before_checkout | Before checkout button | Final offers, discount codes |
product_meta | After product price | Badges, ratings, trust seals |
product_tabs | In product description area | Reviews, 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 %}
Search Implementation
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:
- Ensure your theme includes all required templates (especially
checkout.htmlandthank-you.html). - Include all required
{% bolt_hook %}points in your layout and templates. - Filter bolt items in cart loops with
{% unless item.properties._bolt %}. - Connect your GitHub repository to your developer account.
- Submit for review through the developer portal — our team will test compatibility and provide feedback.