Architecture
Shopify's new Liquid block and partial tags: pages composed in Liquid, not JSON
The Liquid July '26 preview lets a .liquid template own its whole page structure with {% block %}, and refresh regions of server-rendered HTML with {% partial %}. Here's how both work, from Shopify's own reference theme.
Bas Lefeber
Founder, learnshopify.dev · July 29, 2026 · 9 min read
To understand where a Shopify page's structure lives today, you open a JSON template to find the section order, open each section's {% schema %} to find which blocks it accepts, open the block files to find their settings, and hold the whole graph in your head. Nothing about that is wrong. It is just spread across four file types, none of which show you the page.
On July 21, 2026, Shopify shipped a developer preview that takes a different position: put the page structure back in the Liquid template, where you can read it top to bottom. Two new tags do the work, {% block %} and {% partial %}. This post is a working tour of both, using code from Shopify's own reference implementation.
Preview only. Do not put this on a live theme.
Both tags are gated behind the Liquid July '26 developer preview. On a storefront without it, {% partial %} raises Unknown tag 'partial'. Your existing sections, theme settings, and JSON templates are not deprecated and keep working. Read this to understand where theme architecture is heading, and to build on a development store, not to refactor a client's live theme this week.
The block tag, in one breath
The shape is deliberately familiar. If you can read {% render %}, you can read this:
{% block 'name', named_parameter: value %} Body content{% endblock %}It renders blocks/name.liquid, the same folder your theme blocks already live in. Three details carry the weight:
- It takes named parameters. These are ordinary Liquid values the block file reads directly, like
tagorclass. You can also set schema settings inline withblock.settings.<id>: value. - It has body content. Everything between
{% block %}and{% endblock %}is available inside the block as{{ block.content }}. This is the part{% render %}could never do, and it is what makes blocks compose into a page instead of just appearing on one. - The closing tag is mandatory. Even with no body content, you write
{% endblock %}.
That third bullet looks like pedantry until you see what it buys. Because a block wraps content rather than replacing it, blocks nest, and a nested block call is the page layout. Here is the skeleton theme's entire home page template:
{% block 'container' %} {% block 'hello-world' %}{% endblock %}{% endblock %}No JSON. No section. No schema indirection. A container block wrapping a content block, in a file called templates/index.liquid, which is exactly where you would look for it.
What a block file looks like on the other side
The receiving end is a normal theme block with one addition: it renders {{ block.content }} wherever the caller's body content belongs. This is the skeleton theme's container block, the one every template composes through:
{% doc %} Wraps composed page content in a semantic HTML element. Every template composes its page through a container block: the block owns the outer layout element and renders the composed body via {{ block.content }}. @param {string} [tag] - HTML element for the wrapper. Default: 'section'. @example Default section wrapper {% block 'container' %} <h1>{{ page.title }}</h1> {{ page.content }} {% endblock %} @example Override the wrapper element {% block 'container', tag: 'div' %} ... {% endblock %}{% enddoc %} {%- assign tag = tag | default: 'section' -%} <{{ tag }} class="block-container" style="--text-align: {{ block.settings.alignment }};" {{ block.shopify_attributes }}> {{ block.content }}</{{ tag }}>Read the {% doc %} header carefully, because it is doing more than documentation here. @param {string} [tag] declares a named parameter that has nothing to do with the schema. It is not a merchant setting; it is an argument the developer passes at the call site. Blocks now have two distinct input channels: schema settings for merchants editing in the theme editor, and named parameters for developers composing in Liquid. Keeping those straight is the main new concept to absorb.
The doc header is now load-bearing
The preview ships Theme Check rules (3.28.0) that flag mismatches between a block's arguments, its schema, and its {% doc %} declarations, on top of catching syntax errors, excessive complexity, oversized files, and invalid schema structure. Undocumented parameters are a lint failure, not a style opinion.
Everything else stays as you know it. {{ block.shopify_attributes }} still belongs on the root element so the theme editor can identify the block, and the block still carries a {% schema %} for its merchant-facing settings. What it does not carry, in this model, is presets.
A real template: the product page
Composition reads better with something substantial in it. The skeleton theme's product template is one container block holding plain markup and a standard product form:
{% block 'container' %} <div class="product-images"> {% for image in product.images %} {% render 'image', class: 'product-image', image: image %} {% endfor %} </div> <div class="product-info"> <h1>{{ product.title }}</h1> <p>{{ product.price | money }}</p> <p>{{ product.description }}</p> </div> <div class="product-form"> {% form 'product', product %} {% assign current_variant = product.selected_or_first_available_variant %} <select name="id"> {% for variant in product.variants %} <option value="{{ variant.id }}" {% if variant == current_variant %} selected="selected" {% endif %} > {{ variant.title }} - {{ variant.price | money }} </option> {% endfor %} </select> <input type="text" name="quantity" min="1" value="1"> <input type="submit" value="Add to cart"> {{ form | payment_button }} {% endform %} </div>{% endblock %}Notice what did not change: {% render %} for snippets, {% form 'product' %} for the add-to-cart, {% paginate %} on the collection template, the money filter. The Liquid you already know is the Liquid you keep writing. What changed is the container it sits in.
The layout composes the same way. layout/theme.liquid wraps the header and footer blocks in their own containers and drops content_for_layout into a plain <main>, so each template is the composition root for its own page:
<body> {% block 'container', tag: 'div' %} {% block 'header' -%}{%- endblock %} {% endblock %} <main> {{ content_for_layout }} </main> {% block 'container', tag: 'div' %} {% block 'footer' -%}{%- endblock %} {% endblock %}</body>Learn this properly · free lesson
Inside a JSON template: how a page is assembled
This new model only makes sense against the one it sits beside. Take apart a real JSON template and see exactly how sections, blocks, and settings assemble a page today. Free lesson, no signup.
Try this lesson — freeThe partial tag: server-rendered HTML that refreshes itself
The second tag solves a separate problem, and it is the more interesting one. Say a fragment of a page needs to update after an interaction. Today your options are to re-render the whole page, to reach for the Section Rendering API and structure your markup around sections to suit it, or to move that fragment's rendering into JavaScript and give up Liquid for it. Each has a cost, and the third one quietly duplicates your markup in two languages.
{% partial %} names a region of server-rendered HTML so JavaScript can ask the server for a fresh copy of just that region:
{% partial 'name' %}...{% endpartial %}The name in Liquid and the name in JavaScript have to match. That is the whole contract. Here is the skeleton theme's canonical example, a tip card whose sentence refreshes while its heading and control stay put:
<div class="highlight" {{ block.shopify_attributes }}> <h3>{{ 'hello_world.liquid_tips_title' | t }}</h3> {% partial 'liquid-tip' %} {%- if tip_count > 0 -%} {%- assign index = seed | modulo: tip_count -%} {%- for tip in tips limit: 1 offset: index -%} <p class="highlight-description">{{ tip | t }}</p> {%- endfor -%} {%- endif -%} {% endpartial %} <p> <a role="button" tabindex="0" data-refresh-tip> {{ 'hello_world.liquid_tips_refresh' | t }} </a> </p></div>And the JavaScript side is one call:
import { partials } from "@shopify/partial-rendering"; async function refresh() { control.setAttribute("aria-disabled", "true"); try { await partials.refresh("liquid-tip"); flashTip(); } finally { control.removeAttribute("aria-disabled"); }} control?.addEventListener("click", refresh);The payoff is that the tip's rendering logic exists once, in Liquid, on the server. There is no client-side template duplicating it, no JSON endpoint to define, no section restructured to fit a rendering API. Shopify's phrasing for the pair of tags is worth quoting: together they let you compose pages in Liquid and add dynamic storefront interactions without moving rendering into a client-side framework.
One real gotcha in the reference theme
The skeleton theme's own comment flags it: partial rendering replaces the region's DOM node. A reference to an element inside the region captured before the refresh points at a stale, detached node afterwards. Re-query after the swap resolves. This is the same class of bug that bites people using the Section Rendering API, and it will be the most common partial-related question on the forums within a month.
The architecture this implies
The tags are the visible change. The theme structure they enable is the real one. Shopify's reference theme sets rules that read as a genuine break from Online Store 2.0 conventions:
- No sections. No
sections/folder, no{% section %}or{% sections %}tags, no JSON templates, no schemapresets. - No Liquid-embedded assets. No
{% stylesheet %}, no{% javascript %}. CSS and JavaScript live inassets/. - Templates are the composition root. Each
templates/*.liquidwraps its content in one or more container blocks, one per vertical slice. No section is created for markup used by a single page. - Theme Check with zero overrides. The reference keeps a pristine
extends: theme-check:recommendedand fixes the Liquid rather than adding config exceptions.
| Concern | Online Store 2.0 today | Liquid July '26 preview |
|---|---|---|
| Page structure | templates/*.json | templates/*.liquid |
| Composition unit | Section, with a schema | Block, called with {% block %} |
| Passing data in | Schema settings only | Schema settings and named parameters |
| Nesting content | {% content_for 'blocks' %} | {{ block.content }} |
| Refreshing a fragment | Section Rendering API | {% partial %} + partials.refresh() |
Why Shopify is doing this
The stated goal is code that is easier to read, easier to maintain, and easier for AI tools to understand. That last clause is not filler. A coding agent asked to change a product page today has to traverse a JSON template, resolve section types to files, parse schemas, and infer which block accepts which setting, all before it can make an edit. Given one templates/product.liquid that contains the page, it reads the file and edits the file.
It is worth being clear-eyed about the tradeoff, because the JSON template model was not an accident. It exists so merchants can reorder and configure a page in the theme editor without touching code. Structure that moves into a .liquid file is structure a merchant cannot drag around. The preview keeps blocks, schemas, and shopify_attributes precisely so the editor still works on the parts that should be editable, and the reference theme drops presets because a developer-composed page does not need them. Expect the interesting arguments over the next year to be about exactly where that line sits.
How to try it
- Create a development store with the Liquid July '26 developer preview enabled. Without it, the storefront does not know these tags.
- Clone the skeleton theme rc-v2.0.0 branch. Read
AGENTS.mdfirst, thentemplates/product.liquid, thenblocks/container.liquid. That order takes about ten minutes and explains the whole model. - Update Theme Check to 3.28.0 or later so the new Liquid-first rules actually run.
- Build one page. Take a page you have already built with a section and a JSON template, and rebuild it as a
.liquidtemplate composed from blocks. The differences show up fastest on something you already know the shape of.
Do not migrate a production theme. Nothing is being taken away, there is no announced stabilisation date, and preview APIs change. The reason to spend an afternoon on this now is that theme architecture questions, where structure lives, what a merchant controls, what a developer composes, are the durable part of the job. The tags will settle. The judgment about how to arrange a theme is what you are actually building.
Learn this properly · free lesson
Theme Blocks: components you can drop into any section
Theme blocks are the unit this whole model composes from, and they already work in every theme today. Build one from scratch and drop it into a section. Free lesson, no signup.
Try this lesson — freeFrequently asked questions
What is the Shopify Liquid block tag?
{% block 'name', param: value %}...{% endblock %} renders a theme block from blocks/name.liquid directly inside a Liquid template. It works like {% render %}, but it accepts named parameters the block reads as ordinary Liquid values, and the content between the opening and closing tags is available inside the block as {{ block.content }}. It shipped in the Liquid July '26 developer preview on July 21, 2026.
What is the Shopify Liquid partial tag?
{% partial 'name' %}...{% endpartial %} names a region of server-rendered HTML. JavaScript can then call partials.refresh('name') to fetch a freshly rendered copy of that region from Shopify and swap it into the DOM, without a full page reload and without duplicating the rendering logic in a client-side template. The name in Liquid and the name in JavaScript must match.
Do the block and partial tags replace sections and JSON templates?
No. Shopify's changelog is explicit that existing themes keep working with sections, theme settings, and JSON templates, and that this is an alternative composition model rather than a replacement. That said, Shopify's own reference implementation, the skeleton theme rc-v2.0.0 branch, uses no sections and no JSON templates at all, which is a clear signal about direction.
Can I use {% block %} and {% partial %} on a live Shopify store?
Not yet. Both are gated behind the Liquid July '26 developer preview, which you enable on a development store. On a storefront without the preview, {% partial %} raises an Unknown tag 'partial' error. There is no announced stabilisation date, so treat it as something to learn and prototype with rather than ship.
What is the difference between block parameters and block settings?
Schema settings are merchant-facing: they appear in the theme editor and are read from block.settings. Named parameters are developer-facing: they are passed at the call site, like {% block 'container', tag: 'div' %}, and read directly as Liquid variables inside the block. A block should document each named parameter it reads in its {% doc %} header, and Theme Check 3.28.0 flags mismatches between arguments, schema, and doc declarations.
How is {% partial %} different from the Section Rendering API?
The Section Rendering API re-renders a whole section, so your markup has to be organised into sections that match the fragments you want to update. A partial names an arbitrary region inside any Liquid file, so the boundary follows your markup rather than your file structure. Both share one gotcha: the refresh replaces the DOM node, so any element reference captured before the swap points at a stale, detached node afterwards.
Start free
Ready to become a Shopify developer?
You just read how it works. Now write it yourself: real tickets from a live store, in an editor where the storefront updates as you type. Module 1 is free, no card.
Free · No credit card · Your first win in minutes
About the author
Bas Lefeber, Founder, learnshopify.dev
Bas builds learnshopify.dev, where developers learn production-grade Shopify theme development against a live storefront. He writes about Liquid, theme architecture, and the parts of the job that still matter now that AI writes the code.
Keep going in the curriculum
