Theme Blocks & Nesting

Reusable block files every section can opt into — heading, text, button, slide — composed and nested by the merchant up to four levels deep.

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/.vein that any opted-in section can accept. Write a heading, a button, or a slide once; every section that accepts theme blocks composes them.

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.

vein
<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.

vein
<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.

vein
<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.

json
"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