Theme Blocks & Nesting#
Local block types (declared inline in a section's schema — see The {% schema %} Contract) belong to ONE section. A THEME block is the shareable form: a standalone file under blocks/
A theme block file#
A theme block is a .vein file exactly like a section, with two differences: its schema declares "kind": "block", and it renders as a peer — it never inherits the host section's ctx allowance. If a block reads platform data, it declares its own ctx_needs.
<h2 class="tbh tbh--{{ settings.size }}">{{ settings.text }}</h2>
{% schema %}
{
"name": "heading",
"kind": "block",
"icon": "type",
"editor": {"label": "sectionName.heading"},
"settings": [
{"id": "text", "type": "text", "default": "Tell your story",
"editor": {"label": "text", "group": "content"}},
{"id": "size", "type": "select", "default": "lg",
"options": [{"value": "md", "label": "Medium"}, {"value": "lg", "label": "Large"}],
"editor": {"label": "headingSize", "group": "design"}}
]
}
{% endschema %}
Accepting theme blocks#
A section opts in with "blocks": {"accept_theme_blocks": true} and renders the composed list with the {% blocks %} tag — the host renders each block instance with the block's OWN template, settings, and ctx allowance, in the merchant's order, and injects editor selection markers automatically. You place the slot; the platform does the rest.
<div class="hero__content">
{% blocks %}
</div>
{% schema %}
{
"name": "hero",
"blocks": {"accept_theme_blocks": true, "default": [], "types": [], "max": 8},
"settings": [ … ]
}
{% endschema %}
Stored block instances are namespaced: a theme block instance's type is "theme:heading", never bare "heading" — so local types and theme blocks can never collide. Instance shape is identical to local blocks: { id, type, settings, disabled?, label?, locked? }.
Nesting#
A theme block may itself declare a blocks slot ({"accept_theme_blocks": true} on the BLOCK's schema) and place {% blocks %} in its own markup — a slide composed of a heading, a paragraph, and two buttons, each independently editable. A blocks slot on a theme block accepts ONLY theme blocks (no local types). Depth is capped at four levels (a section's blocks are depth 1) — the same cap enforced at save and at render.
<div class="slide" data-slide>
{% if settings.image %}<img class="slide__img" src="{{ settings.image | image_url: width: 640 }}" alt="">{% endif %}
<div class="slide__content">
{% blocks %}
</div>
</div>
{% schema %}
{
"name": "slide",
"kind": "block",
"blocks": {"accept_theme_blocks": true, "max": 6},
"settings": [ {"id": "image", "type": "image", "editor": {"label": "image", "group": "content"}} ]
}
{% endschema %}
Presets that seed blocks#
A section preset carries its starting blocks in a TOP-LEVEL "blocks" field on the preset — not inside preset settings (the gate rejects a "blocks" key in preset settings). Preset blocks may be theme blocks when the section accepts them; each entry is { id, type, settings }, ids stable strings you choose.
"presets": [
{ "name": "Image banner", "category": "Banners",
"settings": {"layout": "editorial"},
"blocks": [
{"id": "banner__b0", "type": "theme:heading", "settings": {"text": "Browse our latest products", "size": "xl"}},
{"id": "banner__b1", "type": "theme:button", "settings": {"label": "Shop the collection", "url": "/shop"}}
]
}
]
Laws that hold everywhere#
Law |
Meaning |
|---|---|
Peer rendering |
A theme block renders with its OWN ctx allowance and settings — never the host section's. Same rule as {% render %} children. |
Strict instance keys |
A stored block instance carries only id, type, settings, disabled, label, locked. Anything else is rejected at save. |
Explicit lists |
A nesting-capable block instance always stores an explicit blocks list — an empty slot is [] on the instance, never absent. |
Setting parity |
Block settings validate and resolve through the SAME per-setting pipeline as section settings — every control type, binding (menus, pickers), and dynamic source works on blocks identically (see Bound Settings). |
Styling |
A theme block ships no |