Custom Liquid
Custom Liquid is the builder's escape hatch: when no built-in section can express a design, you write one — a Liquid template with its own markup and scoped CSS, placed on pages like any other section and configured through the same settings panel.
Built-in sections get theme settings, color schemes, RTL support, product variants and cart integration for free. Custom Liquid gets none of that automatically — it's the right tool for the signature section the catalog can't express, not a general replacement. (The overlap section now covers layered/asymmetric layouts that used to require Liquid.)
Anatomy of a definition
A Custom Liquid definition is store-wide, identified by a handle (like hero-split), and holds:
| Part | What it is |
|---|---|
template | The Liquid source. Instance settings are available as {{ settings.name }}. |
css | A stylesheet scoped to each instance: .self targets the component's wrapper. |
schema | Settings the merchant edits per instance — text, colors, media, selects — rendered in the standard panel. |
defaults | Fallback values for settings. |
data | Commerce data the storefront resolves into the template (below). |
Place instances by adding a Custom Liquid section to a page and picking the handle. Editing a definition updates every instance using it.
Commerce data
Declare what the template needs and the storefront fetches it client-side:
collection— products of a collection (pickable per instance via a setting):collection.title,collection.productswith id, title, url, image, price, formatted price, availability. Always guard with{% if collection.loaded %}and author the empty state — data arrives after first paint.cart— item count and totals.shop— currency.
Interactions: data-tt-action
Custom Liquid output can never contain JavaScript (see security, below). Interactivity comes from CSS — and from a small vetted vocabulary of declarative actions you invoke with data attributes:
| Action | Attributes | Behavior |
|---|---|---|
toggle | data-tt-target, data-tt-class (default tt-open) | Toggle a class on the targets (or the trigger itself); mirrors aria-expanded |
tab | data-tt-tab="group", data-tt-target | Activate one trigger + panel per group via tt-active |
scroll-to | data-tt-target | Smooth-scroll the target into view |
carousel-prev / carousel-next | data-tt-target | Page a scroll container, RTL-aware |
<button data-tt-action="toggle" data-tt-target=".answer">Question?</button>
<div class="answer">…</div>
Style the states in your css: .self .tt-open { … }. Selectors only ever match inside your component — an action can't reach the cart drawer, the checkout, or anything else on the page.
Security model — why some things don't work
Three layers you cannot opt out of:
- Every
{{ }}output is HTML-escaped by the engine. - The rendered HTML is sanitized:
<script>,<iframe>, inline event handlers (onclick=…) and a few other tags are stripped. Templates containing<script>are rejected at save. - CSS is scoped:
.selfbecomes a per-instance class.@keyframesnames are page-global — prefix them with your handle.
Practical consequences: no | raw output of markup from settings, accordions are built with a checkbox inside its <label> plus :has() or the toggle action (not <details>, which is stripped), and behavioral JavaScript belongs in data-tt-action — never in the template.
Authoring rules & examples
The full authoring rules (sanitizer allowlist, the repeatable-content pattern, RTL guidance) ship with a set of proven reference definitions — marquee bar, CTA banner, bento grid, FAQ accordion, trust badges, comparison table, product strip — each tested against the real render pipeline. AI assistants get them through the MCP server (builder_get_liquid_examples); they're the recommended starting point for a first definition rather than a blank template.
Like everything in the builder, Custom Liquid edits stage on your draft and reach shoppers only when you publish.