Part of our guide to Shopify Theme App Extensions, Explained.
The Section Rendering API returns the rendered HTML of one or more sections on their own, so a page can refresh a single section — a cart drawer, a filtered product grid, a swatch change, a paginated list — without reloading the whole page. It's the standard way Online Store 2.0 themes do fast, partial UI updates, and it's the same mechanism behind app-rendered sections.
This guide covers the request shape, the two ways to call it, the parameters that matter, and JavaScript you can adapt. For where this sits in the bigger picture, see the parent hub, Theme App Extensions, explained.
What problem it solves
Say a shopper picks a color swatch and you want the price, availability, and gallery to update. Without this API you'd either re-render the entire page or hand-build the new markup in JavaScript and risk it drifting from your Liquid. The Section Rendering API lets the server render the section with fresh state and hand you back exactly the HTML your theme would have produced — so your Liquid stays the single source of truth. You just swap the returned HTML into the DOM.
Two ways to call it
There are two entry points.
1. Add ?sections= to a standard storefront URL. Request a normal path (the
current page, a collection, a search) and append a comma-separated list of section
IDs. Shopify responds with JSON: a map of section ID → rendered HTML string.
GET /collections/all?sections=product-grid,collection-filters
{
"product-grid": "<div id=\"shopify-section-product-grid\">…</div>",
"collection-filters": "<div id=\"shopify-section-collection-filters\">…</div>"
}
Each value is the full outer markup Shopify renders for that section, including its
shopify-section-* wrapper — so you can replace the existing element outright.
2. Use the /?section_id= render endpoint. For a single section rendered in
isolation (common with app blocks and the theme editor), request the section by its
ID. The response body is the raw HTML for that one section, not JSON.
GET /?section_id=product-grid
Use the sections form when refreshing one or more real sections in the context of
a page (they see the page's objects — the current collection, the cart, applied
filters). Use section_id when you want a section rendered on its own.
The parameters that matter
sections— a comma-separated list of section IDs to render, returned as a JSON object keyed by those IDs. The IDs are the ones in the section's wrapper element (id="shopify-section-<id>"). For sections in JSON templates, that's the key undersectionsin the template file.section_id— a single section ID rendered on its own; the response is HTML, not JSON.- The base URL is not just a formality. The section renders in the context of
that URL. Request
/cart?sections=cart-drawerand the section sees the current cart; request/collections/shoes?sections=product-gridand it sees that collection with any query params (like?filter.v.price.gte=50) applied. Carry the relevant params on the URL and the server does the filtering for you.
A few constraints worth knowing up front:
- It renders sections, not arbitrary snippets or blocks in isolation — target a section.
- Responses are meant to be injected into an existing page, so treat them as partial HTML, not a standalone document.
- It's for reads/renders. Mutations (adding to cart, applying a discount) still go through their own endpoints; you then re-render the affected section to reflect the new state.
A worked example: update on filter change
Here's the common pattern — re-render a product grid and its filters when the
shopper changes a filter, using the sections form so the params drive the result.
async function refreshCollection(url) {
// url already carries the filter params, e.g.
// /collections/shoes?filter.v.price.gte=50&sort_by=price-ascending
const target = new URL(url, window.location.origin);
target.searchParams.set("sections", "product-grid,collection-filters");
const res = await fetch(target.toString());
if (!res.ok) return;
const data = await res.json();
for (const [id, html] of Object.entries(data)) {
const el = document.getElementById(`shopify-section-${id}`);
if (!el) continue;
// parse the returned markup and replace the current section element
const fresh = new DOMParser()
.parseFromString(html, "text/html")
.getElementById(`shopify-section-${id}`);
if (fresh) el.replaceWith(fresh);
}
// reflect the new state in the address bar without a reload
window.history.replaceState({}, "", url);
}
The key move is that the server rendered the grid with the new filters applied. Your JavaScript only swaps elements — it never re-implements the product-card markup, so there's nothing to keep in sync with your Liquid.
A cart-drawer example
The same idea powers a cart drawer that updates after an add-to-cart:
async function addAndRefreshDrawer(variantId, quantity) {
await fetch("/cart/add.js", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: variantId, quantity }),
});
const res = await fetch("/cart?sections=cart-drawer");
const { "cart-drawer": html } = await res.json();
const fresh = new DOMParser()
.parseFromString(html, "text/html")
.getElementById("shopify-section-cart-drawer");
document
.getElementById("shopify-section-cart-drawer")
?.replaceWith(fresh);
}
The mutation (/cart/add.js) and the render (?sections=cart-drawer) are separate
steps: change state, then ask the server to render the section that reflects it.
How this ties to app sections
Because the API renders a section by ID regardless of whether its source is a theme file or an app extension, it's also the delivery path for app-rendered sections. That's the connection to the rest of this pillar: the same endpoint that powers partial page updates is how an app block's markup reaches the page. For the model-level comparison, see App blocks vs. Liquid sections; for the permission an extension-based app declares, see What is the write_themes scope?.
If you're building sections you'd rather not re-implement in JavaScript — and don't want to lose in a theme swap — SectionGuard renders them through a theme app extension, so the Liquid stays the source of truth and the section survives theme updates.