> For the complete documentation index, see [llms.txt](https://analog.groupthought.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://analog.groupthought.com/reference/alpine/section-api.md).

# x-section-api

Directives and magics for the Shopify Section Rendering API: fetch a section's HTML and morph it into the page without a reload.

Directives and magics for the Shopify Section Rendering API: fetch a section's HTML and morph it into the page without a reload.

| Directive / magic                                              | What it does                                                                                                                                                                                |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-section-api="my-section"`                                   | The root. Fetches the named section's HTML and morphs it into this element                                                                                                                  |
| `x-section-api:link`                                           | An anchor inside a root that loads through the section instead of a page load. Pushes a history entry                                                                                       |
| `x-section-api:link.replace`                                   | Replaces the current history entry instead                                                                                                                                                  |
| `x-section-api:link.nopush`                                    | Leaves the history alone                                                                                                                                                                    |
| `x-section-api:form`                                           | A GET form inside a root that submits through the section. Takes the same `.replace` / `.nopush` modifiers                                                                                  |
| `$sectionApi`                                                  | The enclosing root's state and actions: `state` (`'idle'` / `'loading'` / `'refreshing'` / `'error'`), `error`, `sectionId`, `morphConfig`, `update()`, `load()`, `navigate()`, `setHtml()` |
| `data-morph-update="all\|attributes\|children\|none\|regions"` | Sets which parts of an element a morph may update                                                                                                                                           |
| `data-morph-form-state="server"`                               | Makes server HTML drive a form control's live `.value` / `.checked` / `.indeterminate` properties                                                                                           |
| `data-render-region="name"`                                    | Names an independently rendered part of the nearest Section API controller                                                                                                                  |
| `data-render-mode="morph\|replace"`                            | Chooses how a render region updates; the default is `morph`                                                                                                                                 |
| `data-morph-root="name"`                                       | Deprecated alias for `data-render-region`                                                                                                                                                   |

Every `shopify-section` element on the page is bound automatically, so `$sectionApi.update()` works inside any section with no setup. Elements with a `data-shopify` attribute are never morphed. Fetched HTML is cached per URL, and back/forward navigation after a `push` or `replace` reloads the page. The plugin requires `@alpinejs/morph` and the assembly-ui cache plugin.

## Examples

### Reloading the current section

Use `$sectionApi.update()` to re-render the enclosing section with fresh HTML from the server:

```html
<div x-init="
  $watch('$dialog.isOpen', async (isDrawerOpen) => {
    if (isDrawerOpen) {
      await $dialog.waitForAnimations()
      $sectionApi.update({ waitForIdle: true, cachePolicy: 'network-only' })
    }
  })
">
```

The Cart drawer refetches the section every time it opens, so line items and totals come from the server.

### A section that only renders on request

Use `x-section-api="section-id"` to fill an element with a section that renders nothing in the page itself:

```html
<div
  x-section-api="api-product-pickup-drawer"
  @theme:pickup:preload.window="$sectionApi.load({ baseUrl: $event.detail.url, abortPending: false })"
  data-morph-update="children"
>
  …
</div>
```

The pickup drawer, quickview drawer, and add-to-cart notification all work this way. The section's Liquid only outputs content when called through the Section Rendering API, and the drawer fetches it the first time it is needed.

### Links that update the section

Use `x-section-api:link` on an anchor to load its `href` through the section instead of navigating:

```html
<a x-section-api:link href="{{ paginate.next.url }}">
  {{ 'collection.pagination.next' | t }}
</a>
```

Pagination uses the default push mode so each page gets a history entry. The collection filter tags use `x-section-api:link.replace`, so toggling filters rewrites the URL without stacking history entries.

### Forms that update the section

Use `x-section-api:form` on a GET form to submit it through the section:

```html
<form action="{{ filter_url }}" x-section-api:form>
  …
</form>
```

The collection filters form submits this way: the form data becomes the query string, the section refetches with it, and only the results morph. POST forms throw. Use `x-async-form` for those.

### Preloading before the user commits

Use `$sectionApi.load()` to fetch and cache a section's HTML without touching the DOM:

```html
<a
  @pointerenter.once="$sectionApi.load({ baseUrl: '{{ sibling_url }}', abortPending: false })"
  x-section-api:link.replace
  href="{{ sibling_url }}"
>
```

Sibling swatches preload each product on hover, so the click can use the cache. `load()` skips the fetch when the cache already has an entry and joins an in-flight load for the same canonical URL; pass `force: true` to refetch. Preloads should use `abortPending: false` so speculative work cannot cancel an interaction the customer already committed to.

Analog product cards use [x-preloader](/reference/alpine/internals.md#x-preloader-analog) to combine interaction, narrow mobile-viewport, response, and first-image warming. See [Product card architecture](/developer-platform/product-cards.md) for the complete selection lifecycle and priority rules.

### Navigating with a URL

Use `$sectionApi.navigate(url, historyMode)` to update the history and the section in one call:

```html
<select @change="$sectionApi.navigate('{{ option_url }}', 'replace')">
```

The variant option selects use this: picking a variant rewrites the URL to the variant's and re-renders the product section for it. `historyMode` is `'push'`, `'replace'`, or `'none'`; leaving it out also leaves the history alone.

### Protecting elements from the morph

Use `data-morph-update` to control which parts of an element a morph may touch:

| Value        | Attributes |  Children | Typical use                                                                        |
| ------------ | ---------: | --------: | ---------------------------------------------------------------------------------- |
| `all`        |    Updated |   Updated | Normal server-rendered content (the default)                                       |
| `attributes` |    Updated | Preserved | Keep a client-rendered subtree while refreshing its wrapper state                  |
| `children`   |  Preserved |   Updated | Keep Alpine state on a stable wrapper while replacing its server-rendered contents |
| `none`       |  Preserved | Preserved | Protect runtime-owned UI from an ancestor's morph                                  |
| `regions`    |  Preserved | Preserved | Protect a subtree while allowing nested render regions to update independently     |

```html
<div x-cloak x-show="shouldShowLoading" data-morph-update="none">
  {% render 'spinner' %}
</div>
```

The pickup drawer's spinner uses `data-morph-update="none"` so incoming content never deletes it mid-spin. A policy on the morph target itself does not prevent a direct update of that target; it protects the element when an ancestor is being morphed. The `regions` policy provides the same morph protection, but also keeps an enclosing morph region eligible while the Section API updates nested `data-render-region` elements separately. The morph also captures the focused element before it runs and restores focus onto the re-rendered equivalent, so a keyboard user never drops to `<body>`.

For form controls, add `data-morph-form-state="server"` when the response should overwrite live `.value`, `.checked`, or `.indeterminate` properties. Without it, Alpine updates HTML attributes while preserving the user's live form state. When one of those properties changes, the control emits a bubbling `morph:sync-control` event with its `previousValue` and new `value`.

The old `data-morph-skip`, `data-morph-children-only`, `data-morph-skip-children`, and `data-morph-sync` attributes remain supported as deprecated aliases for `none`, `children`, `attributes`, and server-owned form state respectively. New policy attributes take precedence when both forms appear.

### Render regions

Use render regions when different parts of one response need independent update strategies. Live and response regions pair by name. Morphing preserves useful DOM state, while replace mode discards and reinitializes the region's contents:

```html
<section x-section-api="main-collection">
  <aside data-render-region="sidebar">…form controls…</aside>
  <div data-render-region="cards" data-render-mode="replace">…product cards…</div>
</section>
```

One response morphs the sidebar and replaces the cards. When the controller owns any render regions, only those regions update. Content outside them is preserved. The whole section updates only when the controller has no render regions.

Without a `regions` option, every owned leaf region that also appears in the response updates. A conditional region missing from the response is left untouched instead of blocking its matching siblings. Passing `regions` does not enable region rendering. It narrows the update to the listed names and makes that requested set atomic: if any requested region is missing, nothing renders. The `morph` mode updates the existing region element in place. `replace` mode destroys the outgoing Alpine tree, replaces the region element itself, and initializes the incoming tree from scratch.

Use this boundary when client-side code controls part of a section. For example, a product form can put only the server-rendered fields in a region:

```html
<form data-morph-update="regions">
  <!-- Apps may inject inputs or widgets outside the region. -->
  <div data-render-region="product-form-fields">…theme-owned fields…</div>
</form>
```

The Section API can update the theme-owned fields without touching HTML that an app inserted elsewhere in the form. Keep client- or app-owned DOM outside render regions when it must survive updates.

Regions belong to their nearest `x-section-api` controller, so a collection update does not interpret regions inside nested product-card controllers. Names must be unique within a controller. When regions nest in one controller, only the deepest regions normally update. A `data-morph-update="regions"` boundary explicitly keeps a morph-mode ancestor eligible: the ancestor updates its other content while skipping the protected subtree, then each nested region updates separately. The `none` policy does not change region selection. Replace-mode ancestors cannot preserve a protected subtree, so they remain excluded when a nested region is present.

Use `regions` to update a subset of the controller's regions:

```js
$sectionApi.update({url, cachePolicy: 'network-only', abortPending: false, regions: ['related-products']})
```

The related-products section runs two requests in parallel (recommendations and recent views); `regions` keeps each response inside its own block so they cannot overwrite each other. When `regions` matches nothing or the response omits a requested region, nothing renders. A typo won't erase the section. `data-morph-root` and `onlyRoots` remain supported as deprecated aliases.

## Options

`update()` accepts all of these; `load()` accepts everything except the first four:

| Option         | What it does                                                                                                  |
| -------------- | ------------------------------------------------------------------------------------------------------------- |
| `cachePolicy`  | `'cache-and-network'` (default, render cache then refetch), `'cache-first'`, `'cache-only'`, `'network-only'` |
| `htmlMode`     | Default render mode when a region has no `data-render-mode`, or the whole-section mode when no regions exist  |
| `morphConfig`  | Morph callbacks for this update only                                                                          |
| `regions`      | Update only these named `data-render-region` elements; omit it to update all owned regions                    |
| `baseUrl`      | The page URL to fetch; `?section_id=` is appended (default the shop's root URL)                               |
| `url`          | A full request URL used as-is, for endpoints like product recommendations                                     |
| `sectionId`    | Fetch a different section id than the root's                                                                  |
| `batch`        | Combines requests to the same base URL into one `?sections=` request                                          |
| `waitForIdle`  | Waits for an idle moment before updating, capped at 2 seconds                                                 |
| `abortPending` | Aborts the previous request first (default `true`)                                                            |
| `force`        | `load()` only: fetch even when the cache already has an entry                                                 |
| `fetchOptions` | Extra options passed to `fetch()`                                                                             |

Four events fire on `window`, each with `{sectionId, url}` as the detail: `theme:section:load` when a fetch starts, `theme:section:loaded` when it finishes, `theme:section:update` when the DOM changes, and `theme:section:navigate` when `navigate()` completes. Set `window.debugSectionApi = true` to log every fetch, cache hit, and morph to the console.
