Shopify Liquid Partials: A Beginner's Guide
Updating any part of the page without a page reload used to take four steps.
You fetched the section using the Section Rendering API, parsed the response, pulled out the piece you wanted, and wrote the DOM update yourself. Every theme did it, and every theme did it slightly differently.
Shopify Liquid partials replace all four steps into one. You wrap the part of the page that changes in a {% partial %} tag, give it a name, and call one method in JavaScript when it needs to update.
It's part of the same Liquid July '26 developer preview as the block tag, so it isn't production work yet. It's worth learning now, because it will change how theme JavaScript gets written.
In this post, we'll go over what a partial is, how to set up a store to try one, how to refresh a region with refresh(), when to reach for fetch() and apply() instead, what get() and getAll() are for, and a real example you can try, updating the cart count.
Let's go!
What Are Shopify Liquid Partials?
A Shopify Liquid partial is a named region of a page that updates without needing a page reload. Its purpose is the same with the Section Rendering API.
You create one by wrapping the content in a Liquid tag pair:
{% partial 'demo-time' %}
<p>Server time: {{ 'now' | date: '%H:%M:%S' }}</p>
{% endpartial %}
That's the entire Liquid side. The name demo-time is what JavaScript uses later to ask for a fresh copy of that region.
Before partials, updating a region like this meant the Section Rendering API: request the section, parse the HTML out of the response, find the part you wanted, and replace it in the DOM yourself. With a partial, the fetching, the parsing, and the DOM update are handled for you.
How to Set Up a Store for Liquid Partials
As of writing, the partial tag only runs on a store with the Liquid July '26 preview enabled. On any other store the tag isn't recognised and the page fails to render, so this step isn't optional.
You pick the preview when you create the store, in the Dev Dashboard:
- Log in to the Dev Dashboard and select Stores
- Select Create store, then Dev as the store type
- Name the store and pick a plan
- Select Test a feature preview, then Liquid July '26 changes
- Select Create store
Then get a theme. Shopify recommends the Skeleton theme release candidate, which already uses the new tags:
git clone -b rc-v2.0.0 https://github.com/Shopify/skeleton-theme.git
cd skeleton-theme
shopify theme dev --store your-dev-store
Existing themes keep working too, per the developer preview overview. Your sections, theme settings and JSON templates are unaffected by the new tags; just make sure that you are inside the development store with the preview enabled.
The same setup covers the block tag, so if you followed the Liquid Blocks guide you already have this store.
Wrapping Content in a Liquid Partial
Start with the tag on its own, with no JavaScript for now. Put this in a template and load the page:
{% # templates/index.liquid %}
{% partial 'demo-time' %}
<p>Server time: {{ 'now' | date: '%H:%M:%S' }}</p>
{% endpartial %}
The page shows the server time. Reload it and the time changes, because Liquid rendered it again on the server.
That's all the tag does on its own. The content inside is ordinary Liquid, rendered with the rest of the page on first load, and wrapping it changes nothing about how the page renders. All you've done is give the region a name.
The timestamp is there for a reason, and you'll later see why. When you start refreshing the region, a changing time is proof that the HTML came from the server rather than from JavaScript writing a clock into the page.
That name is also what makes the region requestable later. Shopify serves fresh HTML only for regions the tag registered, so writing it yourself gives you nothing to request; you need to use the partial tags.
Refreshing a Liquid Partial with refresh()
Updating a partial takes two things: Shopify's partial rendering module, and a call to one of its methods.
First, import the module. Create the file and import partials from @shopify/partial-rendering:
{% # assets/refresh-server-time.js %}
import { partials } from "@shopify/partial-rendering";
Then load that file from your template as a JavaScript module:
{% # templates/index.liquid %}
<script src="{{ 'refresh-server-time.js' | asset_url }}" type="module"></script>
There's no npm install and no import map to set up. The module resolves on a preview store as it is. The type="module" attribute is required, because the import won't work without it.
Then use it. Add a button to the template, next to the partial you wrapped earlier:
{% # templates/index.liquid %}
<button type="button" data-refresh>refresh()</button>
And call refresh() with the partial's name when it's clicked:
{% # assets/refresh-server-time.js %}
import { partials } from "@shopify/partial-rendering";
const refreshButton = document.querySelector("[data-refresh]");
refreshButton.addEventListener("click", async () => {
await partials.refresh("demo-time");
});
Click the button and the server time updates. refresh() fetches fresh HTML for that region from the current page URL and applies it in one call. No need for additional steps.
refresh() can also take more than one name, so a single call can update several regions at once:
await partials.refresh("product-grid", "product-count");
You can also pass an element instead of a name, which refreshes the partials inside it:
await partials.refresh(document.querySelector("[data-product-list]"));
Per the docs, calling refresh() with no arguments also updates every partial on the page, which is another option you have that affects many regions at once.
Using fetch() and apply() for More Control
refresh() always requests against the current page URL. However, this doesn't work if you need to fetch against a different URL (collection pages, for example, where you want to fetch against the same URL but with sort and filters as URL parameters).
When you need a different URL, use fetch() and apply().
First, import the module in a new file:
{% # assets/fetch-and-apply-server-time.js %}
import { partials } from "@shopify/partial-rendering";
And load that file from your template:
{% # templates/index.liquid %}
<script src="{{ 'fetch-and-apply-server-time.js' | asset_url }}" type="module"></script>
Then add a button for each step, next to the same partial you wrapped earlier:
{% # templates/index.liquid %}
<button type="button" data-fetch>fetch()</button>
<button type="button" data-apply>apply()</button>
Then use the methods. fetch() stores what comes back, and apply() puts it on the page:
{% # assets/fetch-and-apply-server-time.js %}
import { partials } from "@shopify/partial-rendering";
const fetchButton = document.querySelector("[data-fetch]");
const applyButton = document.querySelector("[data-apply]");
let update;
fetchButton.addEventListener("click", async () => {
update = await partials.fetch("demo-time", { url: window.location.href });
});
applyButton.addEventListener("click", () => {
partials.apply(update);
});
fetch() does what it's named after, just fetch the updated content from the server. Clicking it will change nothing on the page. However, if you open the Network Tab of the Browser Console, you'd see it fetching against the URL you've added + the partials as URL parameters.
fetch() can also carry several regions, which keeps them in sync:
const update = await partials.fetch("product-grid", "product-count", { url });
partials.apply(update);
apply() is the action that takes that fetched partial and applies the new content on the region. Thus, when you click apply(), the time updates. Know that it only works after a partial has been fetched.
refresh() vs fetch() and apply()
Both end the same way, with fresh server-rendered HTML on the page. What differs is how much of the request you control.
refresh() |
fetch() and apply() |
|
|---|---|---|
| Calls | One | Two |
| URL requested | The current page URL, always | Any URL you pass in url |
| When the page updates | As soon as the response arrives | When you call apply() |
| Needs a stored result | No | Yes, fetch() returns it and apply() takes it |
| Reach for it when | Adding to cart updates the cart count | Sorting or filtering changes a collection grid |
One question decides it: do you need a different URL or control over when the swap happens? If the answer is no, use refresh().
A collection page is the clearest case for the pair. The sort order and the filters live in the URL, and refresh() can only request the current page, so that update has to go through fetch(). Build that URL from the current page or the routes object rather than hardcoding a path, so it keeps working across locales and markets. As of writing, url is the only option the docs list for fetch().
Reading Partials with get() and getAll()
The last two methods don't update anything. They tell you what's on the page:
const region = partials.get("demo-time");
console.log(region);
console.log(partials.getAll(document.body));
get() returns the first partial with a given name. getAll() returns every partial in the scope you pass it. Neither one fetches or replaces, so the page never changes when you call them.
Real Example: Updating the Cart Count Without a Page Reload
Here's the example from the top of this post, in full. Three files: the header block that holds the region, the product template that posts to the cart, and the JavaScript that ties them together.
First, the header block wraps the cart count in a partial:
{% # blocks/header.liquid %}
<a href="{{ routes.cart_url }}">
{% partial 'header-cart-count' %}
{% if cart.item_count > 0 %}
<sup>{{ cart.item_count }}</sup>
{% endif %}
{% endpartial %}
{{ 'icon-cart.svg' | inline_asset_content }}
</a>
Because the header lives in the layout, that partial is on every page of the store.
Then the product template renders the add-to-cart form, wrapped in a custom element so JavaScript has something to attach to:
{% # templates/product.liquid %}
<script src="{{ 'product-form.js' | asset_url }}" type="module"></script>
<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{% 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">
{% endform %}
</product-form>
Then the JavaScript adds the product to cart and refreshes the cart count:
{% # assets/product-form.js %}
import { partials } from "@shopify/partial-rendering";
class ProductForm extends HTMLElement {
constructor() {
super();
this.form = this.querySelector("form");
this.form.addEventListener("submit", this.handleSubmit.bind(this));
}
async handleSubmit(event) {
event.preventDefault();
await fetch("/cart/add.js", {
method: "POST",
body: new FormData(this.form),
});
await partials.refresh("header-cart-count");
}
}
customElements.define("product-form", ProductForm);
preventDefault() is what keeps the page from navigating, which is the whole reason the count can update in place. Everything after it is the same two steps you've used all post: do the thing, then refresh the region it changed.
Go to a product page and add a product, you should see the count in the header go up with no page reload.
Count the code against the Section Rendering API version: one Liquid tag pair and one line of JavaScript, instead of a fetch, a parse, an extract and a DOM update.
Liquid Partials vs the Section Rendering API
Both update part of a page without a reload. Here's how they differ, as of writing:
| Section Rendering API | Liquid partials | |
|---|---|---|
| What you name | A section | Any region you wrap |
| Server returns | The section's HTML | The named regions' HTML |
| Parsing the response | You write it | Handled for you |
| Updating the DOM | You write it | Handled for you |
| Requesting a different URL | Yes | Yes, with fetch() |
| Several regions in one request | Yes | Yes |
| Availability | Generally available | Developer preview |
Nested Liquid Partials
Partials nest. A named region can sit inside another named region, and both stay addressable:
{% partial 'demo-outer' %}
<p>Outer: {{ 'now' | date: '%H:%M:%S' }}</p>
{% partial 'demo-inner' %}
<p>Inner: {{ 'now' | date: '%H:%M:%S' }}</p>
{% endpartial %}
{% endpartial %}
Refresh the inner one and only the inner content updates. The outer time stays where it was.
Refresh the outer one and both update, because the inner partial is part of the outer partial's HTML, so the server renders it along with everything else inside.
Two Liquid Partials with the Same Name
Nothing stops you from using the same name twice on one page, so it's worth knowing what happens.
Both regions are fetched, and both are applied, each keeping its own content. get() returns only the first one in the DOM, and getAll() returns both.
Avoid it anyway. Nothing about it is documented, this is a developer preview, and the behaviour can change.
Limitations of Liquid Partials
Three things to know before you build anything on this.
- It's a developer preview. Behaviour can change before general availability, so don't ship it on a client store.
- The tag needs the preview enabled. On a store without it, the storefront doesn't recognise the tag.
- A partial update isn't a page load. The browser skips the housekeeping a full navigation does, so production code has more to handle than these examples show. The docs' partial reference lists what that involves, including in-flight requests, loading states and accessibility announcements.
Final Thoughts
Liquid partials come down to two moves. Wrap the region that changes in {% partial %}, then call refresh() with its name when something changes it.
Reach for fetch() and apply() when you need a different URL or control over the timing, and use get() and getAll() when you want to inspect what's on the page rather than change it.
Set up a dev store with the Liquid July '26 changes preview, wrap one region in your theme, and refresh it from a button. Ten minutes on a dev store will tell you more than this post can.
Cheers,
Jan
Shopify Liquid Partials FAQ
What is a Liquid partial in Shopify?
A Liquid partial is a named region of server-rendered HTML, created with the {% partial %} tag, that JavaScript can refresh without reloading the page. It's part of the Liquid July '26 developer preview, alongside the block tag.
What's the difference between {% block %} and {% partial %}?
They arrived in the same preview and solve different problems. {% block %} is about composition, which is what a page is built from. {% partial %} is about updating, which is what changes after the page loads. You can use either one on its own.
How do I update a Liquid partial with JavaScript?
Import the @shopify/partial-rendering module and call partials.refresh('your-partial-name'). That fetches fresh HTML for the region from the current page URL and swaps it in. No npm install or import map is needed on a preview store.
What's the difference between refresh() and fetch() plus apply()?
refresh() does both steps in one call, against the current page URL. fetch() and apply() split them, which lets you request a different URL (a filtered or sorted collection, for example) and choose when the update lands.
Are Liquid partials a replacement for the Section Rendering API?
Eventually they cover the same job with far less code, but not yet. The Section Rendering API is generally available and partials are in developer preview, so partials are for learning and testing right now.
Can a Liquid partial be nested inside another partial?
Yes. Refreshing the inner partial updates only the inner region. Refreshing the outer partial updates the inner one too, because the inner partial is part of the outer partial's HTML.
Want to build Shopify projects with AI?
Join the AI Developer Bootcamp — real workflow, real projects.
Join the Bootcamp →