Script&Theory

§26 / DEVELOPERS

REF: TG–DOC–26 · REV. 1.7

Section Rendering API

Which sections Tangent requests with Shopify's Section Rendering API, and how the cart, variant picker, quick add and filters update the page.

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

SectionRequested byRequest
cart-drawer plus every [data-cart-section] idTangent.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=…&section_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=…&section_id=predictive-search
The collection or search section's id (data-section-id on <facet-filters>)<facet-filters> (facets.js)GET <current path>?<filters>&section_id=…
The collection or search section's id (data-section-id on <load-more>)<load-more> (facets.js)GET <next page url>&section_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&section_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:

example.js
1// assets/theme.js (excerpt)
2sectionIds() {
3 return [
4 'cart-drawer',
5 ...[...document.querySelectorAll('[data-cart-section]')].map((el) => el.dataset.cartSection),
6 ];
7},
8
9async 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: isForm
22 ? { 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},
javascript

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):

  1. 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.
  2. Updates every [data-cart-count] badge from [data-cart-count-value] and re-runs the bump animation.
  3. 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].
  4. Runs reveal() on the cart drawer.
  5. Announces the cartUpdated string and the [data-cart-subtotal] text.
  6. Dispatches cart:updated on document.

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.

example.js
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);
javascript

Details:

  • swapParts() replaces only elements marked data-morph. Keys are per product, for example price-{{ product.id }}, buy-{{ product.id }}, sku-{{ product.id }}, picker-{{ product.id }} and gallery-{{ product.id }}.
  • When the chosen option points to another product (combined listings), the whole [data-product-root] is replaced.
  • withTransition() uses document.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 variant parameter is updated with history.replaceState(), unless the picker has data-update-url="false".
  • After the swap the picker calls window.Shopify?.PaymentButton?.init?.(), Tangent.loadModels() and reveal(), announces the price and button text, and dispatches variant:changed.
  • If the request fails, the browser loads the product URL with option_values as a normal page.

Quick add

<quick-add> requests the quick-add section for the product URL in its data-url:

example.js
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}
javascript

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) inside withTransition(). Parts marked data-morph include toolbar, grid, facets-… and facets-count.
  • Open and expanded filter groups keep their state.
  • The URL is updated with history.pushState().
  • The new count from .toolbar__count is 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

  1. Parse responses with window.Tangent.parseHTML() and copy only the element you need.
  2. After replacing product forms, call window.Shopify?.PaymentButton?.init?.() so dynamic checkout buttons render.
  3. Send cart changes through window.Tangent.cart.request() so they join the theme's queue and re-render the cart.
  4. Announce changes to price, availability or result counts with window.Tangent.announce(message).
  5. Use an AbortController timeout and fall back to a normal page load when a request fails.

Last updated for Tangent 1.7

Get support ↗