> ## Documentation Index
> Fetch the complete documentation index at: https://docs.uselayers.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rendering banners with the SDK

> Render merchandising banners in a Shopify theme by reading data.banners from the SDK collection controller — hero rows above the grid and inline overtake / inject tiles slotted into the product grid by position.

## Overview

This is the SDK equivalent of [Rendering banners in Liquid](/developers/rendering-banners-in-liquid). The browse payload, layout selection, and grid math are identical — but with `@commerce-blocks/sdk`, `client.collection()` does the HTTP call, the request deduplication, and the caching, and exposes the response (including `data.banners`) on a reactive signal you can subscribe to.

Use this version when you've already installed [`@commerce-blocks/sdk`](/sdk/installation) in your theme. For a no-dependencies version, see the [Liquid + Fetch guide](/developers/rendering-banners-in-liquid).

## Prerequisites

* The Layers Shopify app installed and the theme app extension enabled in **Online Store → Themes → Customize → App embeds**.
* The SDK installed and a client created in your theme — see [SDK installation](/sdk/installation).
* At least one merchandising rule with banners attached. See [Add a banner to a rule](/help/merchandising/add-banner-to-rule).

## The banner payload

Banners ship as a top-level, pre-sorted array on the collection controller's `data`:

```ts theme={null}
data.banners
// → Array<{
//     id: string,
//     name: string,
//     mode: 'overtake' | 'inject',
//     link: string | null,
//     sort_index: number,
//     web_media:    { id, src, alt, width, height, source, mediaContentType },
//     mobile_media: { id, src, alt, width, height, source, mediaContentType },
//     web_layout:   { placement: 'hero' | 'inline', width, height, position },
//     mobile_layout:{ placement: 'hero' | 'inline', width, height, position },
//   }>
```

The IDs of the rules that produced them are available on `data._meta.appliedRules` (a flat array of rule ID strings).

Three things to remember:

* **Per-device media and layout.** Pick `web_*` on desktop and `mobile_*` on mobile. They're independent — a hero on web can be an inline tile on mobile.
* **Mode.** `overtake` replaces the product card at `position`. `inject` inserts the banner and shifts trailing products by one cell.
* **Link.** Only `inject` banners can carry a `link`. Wrap them in an anchor; render overtake banners as plain media.

## Step 1 — Subscribe to the collection controller

`client.collection()` returns a reactive controller. Subscribe once and read `data.products` and `data.banners` from the same payload.

```js theme={null}
const collection = window.layers.collection({ handle: COLLECTION_HANDLE })

collection.subscribe(({ data, isFetching, error }) => {
  if (isFetching || error || !data) return
  render({
    products: data.products ?? [],
    banners:  data.banners  ?? [],
  })
})

collection.execute({ pagination: { page: 1, limit: 24 } })
```

The SDK dedupes identical requests, caches the response in `localStorage`, and re-runs the subscriber whenever you call `execute()` with new filter or pagination args.

## Step 2 — Pick the layout for the current device

A banner only has a usable layout when both the media slot **and** the layout for the active device are present. Skip the rest.

```js theme={null}
const mql = window.matchMedia('(max-width: 749px)')
const isMobile = () => mql.matches

function layoutForDevice(banner) {
  const media  = isMobile() ? banner.mobile_media  : banner.web_media
  const layout = isMobile() ? banner.mobile_layout : banner.web_layout
  if (!media?.src || !layout) return null
  return { media, layout, mode: banner.mode, link: banner.link, id: banner.id }
}
```

<Tip>
  Listen for `mql.addEventListener('change', ...)` and re-render when the breakpoint flips. Banners can have completely different layouts per device — a `2×2` web tile may be a hero on mobile.
</Tip>

## Step 3 — Place banners around the products

Hero banners render above the grid. Inline banners slot into the product grid by `position` (row-major, 0-indexed); `overtake` replaces the product at that index, `inject` splices the banner in and shifts trailing products. The response is already sorted by `sort_index`, so no extra sort is needed.

```js theme={null}
function render({ products, banners }) {
  const host = document.querySelector('.collection')
  const grid = document.querySelector('.collection__grid')

  // Clear previously rendered hero banners.
  host.querySelectorAll('.layers-banner--hero').forEach((el) => el.remove())

  // Hero banners — render above the grid.
  for (const banner of banners) {
    const picked = layoutForDevice(banner)
    if (picked?.layout.placement === 'hero') renderHero(host, picked)
  }

  // Inline banners — combined with products into grid cells.
  const cells = buildGridCells(products, banners)
  grid.innerHTML = cells.map(renderCell).join('')
}

function buildGridCells(products, banners) {
  const cells = products.map((p) => ({ type: 'product', product: p }))

  const inline = banners
    .map((b) => ({ banner: b, picked: layoutForDevice(b) }))
    .filter(({ picked }) => picked && picked.layout.placement === 'inline')

  for (const { picked } of inline) {
    const position = picked.layout.position ?? 0
    const cell = {
      type: 'banner',
      mode: picked.mode,
      link: picked.link,
      media: picked.media,
      width: picked.layout.width,
      height: picked.layout.height,
      id: picked.id,
    }

    if (picked.mode === 'overtake') {
      if (position < cells.length) cells[position] = cell
      else cells.push(cell)
    } else {
      cells.splice(Math.min(position, cells.length), 0, cell)
    }
  }

  return cells
}

function renderCell(cell) {
  if (cell.type === 'product') return renderProductCell(cell.product)

  const inner = `<img src="${cell.media.src}" alt="${cell.media.alt ?? ''}" />`
  const style = `--span-x:${cell.width};--span-y:${cell.height}`
  const classes = `layers-banner layers-banner--inline layers-banner--${cell.mode}`

  if (cell.mode === 'inject' && cell.link) {
    return `<a href="${cell.link}" class="${classes}" data-banner-id="${cell.id}" style="${style}">${inner}</a>`
  }
  return `<div class="${classes}" data-banner-id="${cell.id}" style="${style}">${inner}</div>`
}

function renderHero(host, picked) {
  const wrapper = picked.mode === 'inject' && picked.link
    ? `<a href="${picked.link}" class="layers-banner layers-banner--hero" data-banner-id="${picked.id}">`
    : `<div class="layers-banner layers-banner--hero" data-banner-id="${picked.id}">`
  const closing = picked.mode === 'inject' && picked.link ? '</a>' : '</div>'

  host.insertAdjacentHTML(
    'afterbegin',
    `${wrapper}<img src="${picked.media.src}" alt="${picked.media.alt ?? ''}" />${closing}`,
  )
}
```

Use `grid-column: span <width>; grid-row: span <height>` (or the `--span-x` / `--span-y` properties) on banner cells so 2×2 tiles actually cover four grid cells.

## Re-running on filter or pagination changes

The same controller and the same subscriber are reused — just call `execute()` with new args. The banner payload is recomputed by Layers on every request, so banners are always in sync with the current filter set.

```js theme={null}
collection.execute({
  filters: activeFilters,
  pagination: { page, limit: 24 },
})
```

## Fallback rules

Per [Banner Injection](/platform/merchandising/banners#limits-and-behavior):

* A banner without **both** the device's media slot and layout is treated as disabled — `layoutForDevice` returns `null` and you skip it.
* A banner without `web_media` **or** `mobile_media` filled is never enabled by Layers, so it won't appear in the payload in the first place.
* Banners with `mode: "overtake"` ignore any `link` value — only `inject` banners are clickable.

## What the app embed handles for you

The Layers theme app extension does **not** render banners — your theme owns that. The embed *does* handle:

* Authenticating the browse request (token, cart context, identity).
* Sending the [storefront pixel](/shopify-integration/storefront-pixel) events that track shopper engagement on the underlying grid.

Banner click tracking is not automatic. If you need it, send your own [Tracking API](/tracking-api/overview) event from the anchor click handler, keyed on `data-banner-id`.

## Why the SDK over Fetch

| Concern                       | Fetch version                        | SDK version                             |
| :---------------------------- | :----------------------------------- | :-------------------------------------- |
| Driving the request           | `fetch` to `/browse/{handle}`        | `collection.execute()`                  |
| Re-renders                    | DIY DOM updates                      | Signal subscriber                       |
| Caching across nav            | DIY                                  | Restored from `localStorage`            |
| Re-rendering on filter change | Manual orchestration                 | Same controller, just `execute()` again |
| Banner placement              | Same — your theme owns the grid math | Same — your theme owns the grid math    |

## Troubleshooting

**`data.banners` is empty.** No merchandising rule matched the request, or every matching rule had no enabled banners. Confirm at least one active rule on this collection has banners attached and both media slots filled.

**Banners only appear on first load, not after filtering.** Your subscriber probably re-renders products but not banners. Re-read `data.banners` from the same payload on every state update — they ship together.

**Inline banner is at the wrong cell.** `position` is row-major and 0-indexed. Verify the device's `*_layout.position` matches the cell index you expect after flattening the grid to a single array.

**2×2 inline tile renders as a 1×1.** Your CSS isn't honoring `width`/`height`. Apply `grid-column: span <width>; grid-row: span <height>` on banner cells.

## See also

* [Rendering banners in Liquid (Fetch)](/developers/rendering-banners-in-liquid) — the no-dependency equivalent.
* [Banner Injection](/platform/merchandising/banners) — payload reference and dashboard model.
* [Add a banner to a rule](/help/merchandising/add-banner-to-rule) — merchandiser walkthrough.
* [Client methods → `client.collection()`](/sdk/api-reference/client-methods).
* [Browse API](/api-reference/browse) — request and response reference.
