The Section Rendering API hands you the rendered HTML of a theme section, so you can ask Shopify for fresh markup instead of rebuilding it in JavaScript. Append ?sections=cart-drawer to any storefront URL and the response is JSON keyed by section ID. Five sections per request is the ceiling, and Cart API calls can bundle the same rendering into the mutation.
The alternative is assembling that markup yourself, which means you now own money formatting, discount display, unit prices, translations and every theme setting that fed the original Liquid. The section already has all of it.
How do you request a section’s HTML?
Add sections to any page URL. It takes a comma-separated list or an array, and both forms are in Shopify’s Section Rendering API reference:
?sections=main-password-header,sections--1234__header
?sections[]=main-password-header§ions[]=sections--1234__header
The section renders in the Liquid context of the URL you asked for. Request /products/blue-shirt?sections=product-recommendations and the section sees that product; request it against /collections/sale and it sees the collection. Query parameters the full page respects, such as q and page, are respected here too, which is what makes this work for filtered collections and search pagination.
Section IDs are not always the file name. For a statically rendered section the ID is the file name without the extension, so social.liquid is social. Sections placed through a JSON template or a section group get a generated ID, shaped like sections--1234__header or template--5678__image_banner. Read the value in Liquid from section.id, or lift it out of the wrapper, which is always id="shopify-section-[section-id]".
There is a single-section variant, ?section_id=main-password-header, which returns raw HTML instead of JSON. Its failure behaviour differs too, covered below.
async function renderSection(sectionId, path = window.location.pathname) {
const response = await fetch(`${path}?sections=${encodeURIComponent(sectionId)}`);
if (!response.ok) {
throw new Error(`Section request failed: ${response.status}`);
}
const sections = await response.json();
const html = sections[sectionId];
if (html == null) {
throw new Error(`Section ${sectionId} failed to render`);
}
const target = document.getElementById(`shopify-section-${sectionId}`);
if (target) target.outerHTML = html;
}
Build the URL from window.Shopify.routes.root instead of a hard-coded / if the store sells in more than one country or language. Shopify documents that global as the base to use, and says locale-aware URLs are what “give visitors a consistent experience for the language and country that they’ve chosen”.
Why bundle section rendering into a cart request?
Because POST /cart/add.js does not tell you what the cart now looks like. The documented response is the JSON of the line items you just added, so the drawer contents, the header count bubble and the new subtotal all need something else. Without bundling, that something else is a second request.
Bundled section rendering solves it by accepting the same sections parameter inside the mutation. The Cart API reference lists four endpoints that support it: /{locale}/cart/add, /{locale}/cart/change, /{locale}/cart/clear and /{locale}/cart/update. The rendered HTML comes back under a sections key on the cart JSON.
Sections default to rendering in the context of the current page, taken from the HTTP Referer header. Pass sections_url to override it. That value must begin with / and can carry query parameters, so sections_url: "/cart?some_param=foo" is valid. Shopify’s own example requests four sections in one add: cart-items, cart-icon-bubble, cart-live-region-text and cart-footer.
const SECTIONS = ['cart-drawer', 'cart-icon-bubble'];
async function addToCart(variantId, quantity) {
const response = await fetch(`${window.Shopify.routes.root}cart/add.js`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity }],
sections: SECTIONS.join(','),
sections_url: window.location.pathname
})
});
if (response.status === 400) {
// The section parameters were rejected. The line may still have been added,
// so re-read /cart.js before telling the customer anything failed.
return refreshFromCart();
}
const body = await response.json();
applySections(body.sections);
}
function applySections(sections = {}) {
for (const [id, html] of Object.entries(sections)) {
if (html == null) continue;
const target = document.getElementById(`shopify-section-${id}`);
if (target) target.outerHTML = html;
}
}
Predictive search has its own door into the same mechanism. GET /{locale}/search/suggest takes a section_id parameter, marked required in the predictive search reference, and returns that section’s HTML for the query:
/search/suggest?q=bag&resources[type]=product§ion_id=predictive-search
What breaks once real traffic hits it?
Null sections with a 200 status. A section that fails to render, including one that does not exist on the published theme, comes back as null inside an otherwise successful JSON response. Shopify’s wording is that “you should account for this possibility”. Hence the null guard in both samples.
The status code misleads in the other direction too. On the cart endpoints, sections are rendered after the data change is complete, so a rendering error does not alter the response status of the call. And if you pass an invalid value for sections or sections_url, a sections_url missing its leading / being the example Shopify gives, the whole request returns HTTP 400 Bad Request. The docs are blunt about what that does and does not mean: “this doesn’t mean that the rest of the request didn’t succeed.” So a 400 from a cart mutation leaves you in unknown state. Re-read /cart.js before you show the customer an error.
Two smaller traps. You cannot pass section setting values through this API: if the section exists in a template or is statically rendered its saved settings apply, otherwise the defaults do, and no parameter overrides either. And section.index returns nil when a section is rendered through the Section Rendering API, per the Liquid section object reference. If your image loading strategy keys off section.index, it behaves differently here than on a full page load.
The single-section section_id form fails differently again. A section ID that does not exist on the theme returns a 404, with no null body to inspect.
Which response key does the JSON actually use?
Two of Shopify’s own pages disagree, so check before writing the lookup. The Section Rendering API page says the response “includes pairs for each section ID and its corresponding rendered HTML”, but its worked example requests main-password-header and sections--1234__header and then shows a response keyed header and footer. The Cart API page is unambiguous: “Each section can be identified by the same ID that was passed in the request”, and its example response is keyed that way.
I follow the Cart API page and the Section Rendering page’s prose, which agree with each other, and treat the mismatched example as a slip in the docs. Both samples above look sections up by the ID they asked for. Log the raw response against your own theme before you ship and you will not have to take my word for it. One more thing to ignore in those examples: the sample markup writes className on the section wrapper, which is not an HTML attribute at all, so match on the id.
When is this the wrong tool?
When the markup you need is not a section. The API renders sections, and only sections, so a snippet-level update means wrapping that snippet in a section you may not otherwise want. On a headless storefront none of this applies; the Storefront API cart flow is the equivalent, and it returns data instead of HTML. And if your theme’s cart is already a client-rendered component holding its own state, swapping outerHTML underneath it will fight whatever framework you chose.
Then there is the cap. Five sections sounds generous until you count the regions that change after one add to cart, and Shopify’s own example is already using four of them.
Use it for cart drawers, count bubbles, facet-filtered collection grids, search pagination and predictive search. Any time the trigger is a cart mutation, prefer bundled section rendering over a separate ?sections= call: it is one request instead of two, and because the sections render after the data change completes, the HTML you get back describes the cart you just changed. Write the null guard first. If you find yourself wanting to pass settings through the request, or you are already rendering the cart in React, you have outgrown the API and should say so before building around it.
If you want the theme architecture underneath all this, our write-up on theme blocks and section blocks covers where sections stop and blocks start. We build and maintain Shopify themes where this pattern carries the cart, search and filtering, and we pick up storefronts where an earlier build reimplemented cart markup in JavaScript and drifted out of sync with Liquid. That is the sort of work our Shopify development team does.