Theme blocks are Liquid files in your theme’s /blocks folder. Any section can render them, they nest inside one another, and a theme is capped at 300 of them. Section blocks are declared inside a single section’s {% schema %}, work only in that section, and cannot nest. One section cannot use both.
That last rule is the one that catches people mid-build. You add a slick new theme block to an existing section, the theme editor stops offering the section’s own blocks, and you now own a rewrite you did not plan. Worth knowing before you start rather than after.
Shopify theme blocks vs section blocks, feature by feature
A section block is a type entry in the blocks array of one section’s schema, with its own settings. It exists because that section says so. Move the markup to a second section and you copy the schema definition across with it, then maintain two of them.
A theme block is a file. blocks/text.liquid carries its own {% schema %}, its own settings, its own {% stylesheet %}, and any section that opts in can render it. It also has something section blocks never got: children. A theme block’s schema can include a blocks array, so a slide can hold a heading, a button and an app block, each with their own settings.
Shopify’s blocks documentation is blunt about the ceiling on the older style: section blocks “only support a single level of hierarchy, and cannot be nested”. If you want a slideshow whose slides hold arbitrary content instead of three fixed settings, theme blocks are the only way there.
The trade-off is scope. Section blocks are private by construction, so nobody drops your bespoke comparison-table row into the footer. Theme blocks are available everywhere unless you deliberately restrict them.
How do you write a theme block?
One file, one schema, at least one preset.
{% comment %} blocks/promo-text.liquid {% endcomment %}
<div class="promo-text promo-text--{{ block.settings.alignment }}">
{{ block.settings.text }}
</div>
{% stylesheet %}
.promo-text--left { text-align: left; }
.promo-text--center { text-align: center; }
.promo-text--right { text-align: right; }
{% endstylesheet %}
{% schema %}
{
"name": "Promo text",
"tag": "div",
"class": "promo-text-wrapper",
"settings": [
{ "type": "richtext", "id": "text", "label": "Text" },
{ "type": "text_alignment", "id": "alignment", "label": "Alignment" }
],
"presets": [
{ "name": "Promo text" }
]
}
{% endschema %}
Two things bite here. Presets are not decoration: per the quick start, presets “need to be defined in order for the theme block to be available” in the editor, so a block without one is invisible to merchants no matter how correct the rest of it is. And a theme block cannot read variables created outside itself. It gets block and section, not the assign you wrote three lines above the render point.
How does a section render theme blocks?
The section opts in through its schema, then marks the drop zone with {% content_for 'blocks' %}.
{% comment %} sections/flexible-banner.liquid {% endcomment %}
<div class="flexible-banner color-{{ section.settings.color_scheme }}">
{% content_for 'blocks' %}
</div>
{% schema %}
{
"name": "Flexible banner",
"blocks": [{ "type": "@theme" }, { "type": "@app" }],
"settings": [
{
"type": "color_scheme",
"id": "color_scheme",
"label": "Colour scheme",
"default": "scheme-1"
}
],
"presets": [{ "name": "Flexible banner" }]
}
{% endschema %}
"@theme" accepts every non-private theme block, "@app" accepts app blocks from installed apps, and naming a type instead ({ "type": "slide" }) narrows the picker to that one. To keep a block out of the general pool, prefix its filename with an underscore. Shopify’s targeting guide explains that _slide.liquid is excluded from @theme and only appears where a schema names { "type": "_slide" } explicitly.
Notice what is missing from that section file. No {% for %} loop, no {{ block.shopify_attributes }}. Shopify renders the children in block_order for you. Section blocks still need the manual version:
{% for block in section.blocks %}
{% case block.type %}
{% when 'quote' %}
<blockquote {{ block.shopify_attributes }}>
{{ block.settings.text }}
</blockquote>
{% endcase %}
{% endfor %}
block.shopify_attributes prints the data attributes the theme editor’s JavaScript uses to identify a block and listen for events. It returns nothing outside the editor, so omitting it breaks click-to-select in the customiser and nothing on the live storefront. Do not key any logic off block.id either; the docs warn it is generated dynamically and subject to change.
What are static blocks for?
Sometimes a block belongs in a fixed spot and must not be reorderable. The static form of content_for handles that:
{% content_for 'block', type: 'button', id: 'primary-cta', color: 'red' %}
type and id are required. Anything else you pass, color in that example, is available inside the block. Merchants can hide a static block but cannot delete it, which makes this the right tool for an add-to-cart button or a price line inside a product section. Static blocks also sit outside the per-section block count.
The limits you will actually hit
A theme can hold at most 300 theme blocks, and every .liquid file in /blocks counts, “including AI-generated theme blocks and blocks that aren’t currently referenced by any section or template”. Dead files are not free.
JSON templates render up to 25 sections, and each section takes up to 50 blocks. The max_blocks attribute in a section schema lowers that ceiling but never raises it. Separately, limit in a section schema restricts how often a section can appear in one template, and it accepts only 1 or 2.
Shopify’s own best-practice guidance is worth heeding before you go block-happy: “Avoid providing blocks that are too granular. Granularity adds complexity to the theme code and to the merchant editing experience.” Twelve blocks in a picker is a worse editor than four good ones.
Can one section use both kinds?
No. The documentation says section blocks “can not currently be used in the same section as Theme blocks”, and that has not changed as of September 2026. The word “currently” is Shopify’s, so treat the restriction as a present fact rather than a permanent one.
Converting a section is mechanical. Lift each entry out of the section’s blocks array into its own file in /blocks, carrying the settings across unchanged. Add a preset to each. Delete the {% case %} loop and put {% content_for 'blocks' %} in its place. Replace the schema’s local definitions with @theme or an explicit list of the new types.
The part to be careful about is merchant content. A live JSON template stores each block as a type string plus its settings under block_order, so name each new file exactly as the old block type was named, and keep every setting id identical. Then duplicate the theme, apply the change to the copy, and open a real product or collection page in the editor before you publish. Migrating the code is quick. Discovering afterwards that four hundred merchant-authored blocks lost their text is not.
Which should you use?
For anything new, theme blocks. Reuse and nesting are worth the extra files, and new-generation themes are built on them. Reach for section blocks only when the content genuinely belongs to one section, will never be reused, and needs no hierarchy: a row in a bespoke comparison table, a link in a specific footer column. On an existing theme, leave working section blocks where they are and introduce theme blocks in new sections, because the two cannot share a section and a rewrite you did not need is a rewrite you should not do.
If a block’s content is really a content model rather than a few settings, define it once and reference it instead of duplicating fields per block. Our note on choosing between metaobjects and metafields covers where that line sits. And none of this applies on a headless build, where the equivalent decision is which framework owns rendering; that is the ground covered in our Hydrogen and Next.js comparison.
Whoooop builds and maintains Shopify themes, including converting older section-block themes onto theme blocks without losing merchant content. If you want a second pair of eyes on a theme architecture before you commit to it, that is the kind of thing our Shopify development work covers.