Shopify Tutorial

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:

  1. Log in to the Dev Dashboard and select Stores
  2. Select Create store, then Dev as the store type
  3. Name the store and pick a plan
  4. Select Test a feature preview, then Liquid July '26 changes
  5. 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.

The same page on localhost twice, on first load and after a reload, with the server time changing from 14:32:07 to 14:32:19
The tag alone changes nothing, except that the time is rendered again

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.

The page before and after clicking the refresh button, with only the server time changing and the rest of the page identical
One call, one region updated, no page reload

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);
});
The demo page on localhost showing the server time with a fetch button and an apply button below it, before anything is clicked
The page you just built, before anything is clicked

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.

The page with the server time unchanged after clicking fetch, and the Network tab below showing a request to the page URL with the partial name as a URL parameter
The request went out, and the page has not changed

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.

The page before and after clicking apply, with the server time updating to the value fetched a moment earlier
The fetched value lands when you call apply()

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.

A product page before and after clicking Add to cart, with the header cart count going from 1 to 2 while the rest of the page stays the same
The cart count updates in the header, with no 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.

Side by side code comparison: the Section Rendering API version in four numbered JavaScript steps, and the Liquid partials version as a three line tag pair plus one refresh call
The same feature, written both ways

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.

  1. It's a developer preview. Behaviour can change before general availability, so don't ship it on a client store.
  2. The tag needs the preview enabled. On a store without it, the storefront doesn't recognise the tag.
  3. 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 →