§26 / DEVELOPERS
REF: TG–DOC–26 · REV. 1.7
Section Rendering API
TANGENT 1.7 / SHOPIFY OS 2.0 / UPDATED 2026-10-19
Shopify's Section Rendering API returns the rendered HTML of a section for a given URL. Tangent uses it for the cart, variant selection, quick add, filters and several lazy-loaded blocks. All markup comes from the section's Liquid; there are no client-side templates. If a request fails, the element falls back to a normal page load or navigation.
Sections requested by the theme
| Section | Requested by | Request |
|---|---|---|
cart-drawer plus every [data-cart-section] id | Tangent.cart (theme.js) | sections and sections_url in the body of /cart/add.js and /cart/change.js, or GET /cart?sections=… |
The product section's id (data-section-id on <variant-picker>) | <variant-picker> (product.js) | GET <product url>?option_values=…§ion_id=… |
pickup-availability | <pickup-availability> (product.js) | GET /variants/<id>/?section_id=pickup-availability |
quick-add | <quick-add> (extras.js) | GET <product url>?section_id=quick-add |
predictive-search | <predictive-search> (search.js) | GET /search/suggest?q=…§ion_id=predictive-search |
The collection or search section's id (data-section-id on <facet-filters>) | <facet-filters> (facets.js) | GET <current path>?<filters>§ion_id=… |
The collection or search section's id (data-section-id on <load-more>) | <load-more> (facets.js) | GET <next page url>§ion_id=… |
card-render | <recently-viewed> (recent.js) | GET /products/<handle>?section_id=card-render |
collection-preview | <style-quiz> (extras.js) | GET <collection url>/<tags>?section_id=collection-preview |
cart-recommendations | <cart-recommendations> (cart.js) | GET /recommendations/products?product_id=…&limit=8§ion_id=cart-recommendations |
| The recommendations section's id | <product-recommendations> (extras.js) | GET /recommendations/products?section_id=…&product_id=…&limit=…&intent=related or complementary |
Paths above use the default routes. The code reads them from window.Tangent.routes (routes.root, routes.cart, routes.cartAdd, routes.cartChange, routes.predictiveSearch), so they include the market or locale prefix. The product and collection section ids are template-specific ({{ section.id }}), not the fixed strings main-product or main-collection.
Cart requests
All cart writes go through window.Tangent.cart.request(url, body). Requests run one at a time in a promise queue (cart.queue), so a slow response cannot render over a newer one. A failed request does not block the queue.
cart.send() adds the section ids to every request:
1// assets/theme.js (excerpt)2sectionIds() {3 return [4 'cart-drawer',5 ...[...document.querySelectorAll('[data-cart-section]')].map((el) => el.dataset.cartSection),6 ];7},89async send(url, body) {10 const isForm = body instanceof FormData;11 const sections = [...new Set(this.sectionIds())];12 if (isForm) {13 body.append('sections', sections.join(','));14 body.append('sections_url', window.location.pathname);15 } else {16 body.sections = sections.join(',');17 body.sections_url = window.location.pathname;18 }19 const res = await fetch(url, {20 method: 'POST',21 headers: isForm22 ? { Accept: 'application/json' }23 : { 'Content-Type': 'application/json', Accept: 'application/json' },24 body: isForm ? body : JSON.stringify(body),25 });26 const data = await res.json();27 if (!res.ok || data.status) {28 const message = data.description || data.message || strings.cartError;29 throw new Error(typeof message === 'string' ? message : strings.cartError);30 }31 this.render(data.sections);32 return data;33},The cart page section (sections/main-cart.liquid) sets data-cart-section="{{ section.id }}" on its body, so it is included on the cart page.
Rendering the response
cart.render(sections):
- For each returned section, replaces the children of every
[data-cart-section="<id>"]and#shopify-section-<id> [data-cart-root]with the new[data-cart-root]children. - Updates every
[data-cart-count]badge from[data-cart-count-value]and re-runs thebumpanimation. - Restores focus to the previously focused element by id. If that element is gone and focus was in the cart, focus moves to
[data-cart-title]. - Runs
reveal()on the cart drawer. - Announces the
cartUpdatedstring and the[data-cart-subtotal]text. - Dispatches
cart:updatedondocument.
cart.refresh() fetches the same sections with GET <routes.cart>?sections=… and passes them to render(). <product-form> calls it after a failed add, because a rejected request can still change the cart.
Variant selection
<variant-picker> sends the selected option value ids (data-option-value-id on the checked inputs) with option_values, and requests its own section. Using option value ids supports products with up to 2,048 variants and combined listings.
1// assets/product.js (excerpt)2const params = new URLSearchParams({ option_values: ids.join(','), section_id: this.sectionId });3const res = await fetch(`${productUrl}?${params}`, { signal: controller.signal });4const doc = parseHTML(await res.text());5const nextRoot = doc.querySelector('[data-product-root]');6const update = () => {7 if (isNewProduct) {8 root.replaceWith(nextRoot);9 } else {10 swapParts(root, nextRoot);11 root.dataset.featuredMediaId = nextRoot.dataset.featuredMediaId;12 root.dataset.variantId = variantId;13 }14};15const transition = quiet ? (update(), null) : withTransition(update);Details:
swapParts()replaces only elements markeddata-morph. Keys are per product, for exampleprice-{{ product.id }},buy-{{ product.id }},sku-{{ product.id }},picker-{{ product.id }}andgallery-{{ product.id }}.- When the chosen option points to another product (combined listings), the whole
[data-product-root]is replaced. withTransition()usesdocument.startViewTransition()when it exists and reduced motion is off.- Each section has one request in flight. A new choice aborts the previous request.
- The URL's
variantparameter is updated withhistory.replaceState(), unless the picker hasdata-update-url="false". - After the swap the picker calls
window.Shopify?.PaymentButton?.init?.(),Tangent.loadModels()andreveal(), announces the price and button text, and dispatchesvariant:changed. - If the request fails, the browser loads the product URL with
option_valuesas a normal page.
Quick add
<quick-add> requests the quick-add section for the product URL in its data-url:
1// assets/extras.js (excerpt)2const controller = new AbortController();3const timer = setTimeout(() => controller.abort(), 10000);4try {5 const url = `${this.dataset.url}${this.dataset.url.includes('?') ? '&' : '?'}section_id=quick-add`;6 const res = await fetch(url, { signal: controller.signal });7 if (!res.ok) throw new Error(`HTTP ${res.status}`);8 const content = parseHTML(await res.text()).querySelector('[data-quick-add-content]');9 if (!content) throw new Error('Missing quick add content');10 body.replaceChildren(...content.childNodes);11 modal.open(this.button);12 window.Shopify?.PaymentButton?.init?.();13 window.Tangent.loadModels?.(body);14} catch {15 toast(strings.quickAddError, { error: true });16 setTimeout(() => this.fallback(), 1200);17} finally {18 clearTimeout(timer);19}fallback() navigates to the product page.
Filters and sorting
<facet-filters> requests the current path with the filter query and its own section id:
- The request is aborted after 12 seconds.
- The response is applied with
swapParts(this, doc)insidewithTransition(). Parts markeddata-morphincludetoolbar,grid,facets-…andfacets-count. - Open and expanded filter groups keep their state.
- The URL is updated with
history.pushState(). - The new count from
.toolbar__countis announced. - If the request fails, the browser loads the filtered URL as a normal page.
<load-more> requests the next page with the same section id and appends the new [data-grid] > li items to the current grid.
Guidelines for your own code
- Parse responses with
window.Tangent.parseHTML()and copy only the element you need. - After replacing product forms, call
window.Shopify?.PaymentButton?.init?.()so dynamic checkout buttons render. - Send cart changes through
window.Tangent.cart.request()so they join the theme's queue and re-render the cart. - Announce changes to price, availability or result counts with
window.Tangent.announce(message). - Use an
AbortControllertimeout and fall back to a normal page load when a request fails.
Last updated for Tangent 1.7