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

# Templates, bindings and actions

> How the kit's elements bind data into your markup: data-qb-* bindings, repeated templates, actions, reflected state and money formatting.

The elements never generate markup. You write the HTML; the elements fill it in through a handful of `data-qb-*` attributes and reflect their state as attributes you can style.

```html theme={null}
<qb-product slug="cotton-tee">
  <h1 data-qb-text="product.name"></h1>
  <qb-product-image width="800" height="800"></qb-product-image>
  <p data-qb-text="price.display"></p>
  <qb-add-to-cart><button type="button">Lägg i varukorg</button></qb-add-to-cart>
</qb-product>
```

## Bindings

Put these on any element **inside** a component. Values are **paths** into the component's scope, never expressions: there is no evaluator and nothing in a template can execute.

| Attribute | Effect |
| - | - |
| `data-qb-text="product.name"` | Sets `textContent` |
| `data-qb-html="product.description"` | Sets `innerHTML`. Merchant descriptions are HTML |
| `data-qb-attr="href:product.href, title:product.name"` | Sets attributes. `null` / `false` removes one, `true` sets it empty |
| `data-qb-class="on-sale:price.onSale"` | Toggles a class |
| `data-qb-show="cart.empty"` / `data-qb-hide="cart.empty"` | Toggles the `hidden` attribute |
| `data-qb-value="item.quantity"` | Sets an input's `value`, only when it differs, so the caret is not disturbed |

Paths walk dots and array indices (`product.images.0.url`). A missing branch resolves to `undefined` and renders empty instead of throwing, so a template written for a product with images does not break on one without.

<Tip>
  Write `hidden` in the markup on anything with `data-qb-show`, or it is visible for a moment before the first bind:

  ```html theme={null}
  <p data-qb-show="cart.empty" hidden>Din varukorg är tom.</p>
  ```
</Tip>

### Which element is bound

A component's **own** attributes are configuration; bindings apply to its descendants. Scope-owning elements (`<qb-product>`, `<qb-product-list>`, `<qb-options>`, `<qb-option-values>`, `<qb-cart>`, `<qb-cart-items>`, `<qb-order-confirmation>`) stop the walk, so a binding written on one of them **itself** is never applied. The other elements (`<qb-add-to-cart>`, `<qb-product-image>`, `<qb-checkout-button>`, `<qb-checkout>`, `<qb-cart-count>`, `<qb-seo>`) are bound by the enclosing component like any element.

```html theme={null}
<qb-add-to-cart data-qb-show="hasOptions">…</qb-add-to-cart>  <!-- works -->
<qb-options data-qb-show="hasOptions">…</qb-options>          <!-- ignored: put it on a wrapper -->
```

Nothing inside a `<template>` is ever bound in place; only its clones are.

## Repeats

Every list element (`<qb-product-list>`, `<qb-options>`, `<qb-option-values>`, `<qb-cart-items>`) clones its `<template>` once per item. Three rules decide whether a list renders at all:

<Steps>
  <Step title="The template is a direct child of the repeating element">
    The element scans its own children for the `<template>`. A template nested inside a `<ul>` or a `<div>` is never found; the element warns once in the console and renders nothing.
  </Step>

  <Step title="Rows are inserted after the template, as siblings">
    So the repeating element **is** the list container. Style `qb-product-list` itself as the grid and give the template a root of `<article>`, `<a>` or `<div>`. A heading or empty-state paragraph written before the template stays where it is.
  </Step>

  <Step title="A template may have several root elements">
    `<dt>` and `<dd>`, or two `<td>`s, all belong to the same item.
  </Step>
</Steps>

<CodeGroup>
  ```html Correct theme={null}
  <qb-product-list class="grid" limit="12">
    <template>
      <article class="card">
        <h3 data-qb-text="product.name"></h3>
      </article>
    </template>
  </qb-product-list>

  <style>
    qb-product-list.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 1.5rem; }
  </style>
  ```

  ```html Renders nothing theme={null}
  <qb-product-list limit="12">
    <ul class="grid">
      <template>  <!-- not a direct child: never found -->
        <li data-qb-text="product.name"></li>
      </template>
    </ul>
  </qb-product-list>
  ```
</CodeGroup>

Rows are **keyed** (product id, option group id, option value id, cart line id) and reused across updates. That keeps a cart quantity `<input>` focused while its line re-renders, and keeps a swatch's DOM stable across selection changes.

## Actions

One attribute, `data-qb-action`, resolved by the nearest enclosing component that knows the verb. Both `click` and `change` dispatch it, so a `<select>` or a quantity `<input>` can carry one. On an `<a>` or a submit button the default is prevented.

| Verb | Handled by | Does |
| - | - | - |
| `select` | `<qb-option-values>` | Choose that row's option value |
| `add-to-cart` | `<qb-product>` | Add the selected variant |
| `buy-now` | `<qb-product>` | Add the selected variant and go straight to the hosted checkout. Reads `data-qb-success-url`, `data-qb-back-url`, `data-qb-theme` and `data-qb-no-redirect` off the button |
| `reset` | `<qb-product>` | Clear the selection |
| `remove`, `increment`, `decrement` | `<qb-cart-items>` | That row's cart line |
| `clear` (alias `clear-cart`), `refresh` | `<qb-cart>` | The whole cart |
| `set-currency` | `<qb-currency-select>` | Switch to that template row's currency. See [Currencies](/kit/concepts/currencies) |
| `accept-all`, `reject-all`, `save`, `open-settings`, `close-settings` | `<qb-consent-banner>` | The cookie consent decision; `save` reads the `data-qb-consent` checkboxes. See [Consent and analytics](/kit/concepts/consent-and-analytics) |

A verb nothing handles is reported once in the console on a click, so a typo is a message instead of a button that silently does nothing.

### Quantity for add-to-cart

In precedence order:

1. `data-qb-quantity="3"` on the clicked element.
2. The nearest `input[data-qb-quantity-input]` **inside `<qb-add-to-cart>`** (for a plain `data-qb-action="add-to-cart"` button, the search runs from the click up to `<qb-product>`).
3. The `quantity` attribute of `<qb-add-to-cart>`.
4. `1`.

```html theme={null}
<qb-add-to-cart>
  <input type="number" min="1" value="1" data-qb-quantity-input aria-label="Antal">
  <button type="button">Lägg i varukorg</button>
</qb-add-to-cart>
```

<Note>
  * A quantity input placed **next to** the wrapper rather than inside it is ignored.
  * `data-qb-quantity` on a `<button>` is missed when the click lands on an icon inside it; prefer the input.
  * A blank or `0` input counts as 1.
  * In cart rows the attribute is different: `input[data-qb-quantity]` (no suffix) is the bound quantity of an existing line and commits on `change`, so typing `12` does not first send `1`. `0` removes the line.
</Note>

## Styling on reflected state

The elements ship no CSS. Everything they know is reflected as an attribute, so a loading skeleton, a dimmed swatch or a disabled checkout button is a stylesheet's job:

```css theme={null}
/* Custom elements are display: inline until you say otherwise. */
qb-product, qb-cart, qb-cart-items, qb-options { display: block; }

[state="loading"] { opacity: .5; }
[state="error"] { outline: 1px solid crimson; }

qb-product[empty] { display: none; }               /* no such product */
qb-product[incomplete] .add-hint { display: block; }
qb-options[empty] { display: none; }               /* a simple product */

.values [data-selected="true"] { border-color: currentColor; }
.values [data-available="false"] { opacity: .35; text-decoration: line-through; }

qb-add-to-cart[pending] button::after { content: "…"; }
qb-checkout-button[blocked] button { opacity: .4; }

qb-cart-count[zero] { visibility: hidden; }
qb-cart[empty] .totals { display: none; }
qb-cart-items [data-available="false"] { opacity: .5; }

qb-product-image[empty] { background: var(--placeholder); aspect-ratio: 1; }
qb-order-confirmation[status="pending"] .done { display: none; }
```

| Element | Reflects |
| - | - |
| `qb-product` | `state` (`idle`, `loading`, `ready`, `error`), `empty`, `complete` / `incomplete`, `buying` |
| `qb-options` | `empty`; per row `data-option-id`, `data-chosen` |
| `qb-option-values` | `empty`; per row `data-value-id`, `data-selected`, `data-available`, `aria-pressed` |
| `qb-add-to-cart`, `qb-buy-now` | `pending`, `blocked`, `state`; inner buttons' `disabled` |
| `qb-product-list` | `state`, `empty`; per row `data-product-id` |
| `qb-product-image` | `empty` |
| `qb-cart`, `qb-cart-items` | `state`, `empty`, `pending`; per row `data-item-id`, `data-available` |
| `qb-cart-count` | `zero`, `state` |
| `qb-checkout-button` | `empty`, `blocked`, `pending`, `state`; inner buttons' `disabled` |
| `qb-checkout` | `state` |
| `qb-order-confirmation` | `status` (`pending`, `completed`, `failed`, `timeout`, `unknown`), `state` |

<Warning>
  Never write `disabled` into an option value template. Unavailable values must stay clickable: clicking one keeps the new choice and clears whatever contradicts it. Dim them with `[data-available="false"]` instead.
</Warning>

## Money

`price.display`, `item.lineTotalFormatted`, `cart.totalFormatted` and friends are localized strings, because an HTML binding cannot leave formatting to the app the way React does. They format with the currency the response states (`product.currency`, `cart.currency`), so the right symbol follows a [currency switch](/kit/concepts/currencies) with nothing configured, and with the configured `locale`. The configured `currency` is only a fallback for a platform older than `product.currency` (see [Set the currency](/kit/web-components/setup#set-the-currency)).

The raw minor-unit integers are always in scope too (`price.amount`, `item.lineTotal`, `cart.total`) for your own formatter. `Quickbutik.formatMoney(129900, "SEK", { locale: "sv-SE" })` is the formatter the elements use (`"1 299,00 kr"`).

## Scope paths

The full list of what each component puts in scope is in the [elements reference](/kit/reference/elements). The ones you will use most:

| Scope | Paths |
| - | - |
| `product.*` | The `Product` plus `product.image` (first image record). In list rows also `product.priceFormatted`, `product.href` |
| `price.*` | `amount` (null until a variant is pinned), `formatted`, `display` (exact once pinned, range before), `compareAt.amount`, `compareAt.formatted`, `onSale`, `min.*`, `max.*`, `isRange`, `currency` |
| `variant.*` | `id`, `sku`, `stock`, `soldOut` (only when stock is tracked and exhausted), `preorder`, `selected` |
| `group.*` / `value.*` | `id`, `name`, `position`, `values`, `selectedValueId`, `chosen` / `id`, `name`, `optionId`, `selected`, `available`, `unavailable`, `variantIds` |
| `cart.*` | The `Cart` plus `empty`, `subtotalFormatted`, `totalFormatted`, `totalTaxFormatted`, `totalDiscountFormatted` |
| `item.*` | The `CartItem` plus `unitPriceFormatted`, `lineTotalFormatted`, `compareAtFormatted`, `discountFormatted`, `onSale` |
| `list.*` | `loading`, `error`, `count`, `empty`, `hasMore`, `nextCursor` |
| `confirmation.*` | `status`, `kind`, `orderNumber`, `loading`, `completed`, `failed`, `timedOut`, `processing`, `error` |
| `currency.*` / `option.*` | In `<qb-currency-select>`: `code`, `base`, `mode`, `chargeCurrency`, `rate`, `count` / in a template row: `code`, `label`, `name`, `mode`, `rate`, `selected`, `base` |
| `consent.*` | In `<qb-consent-banner>`: `status`, `undecided`, `decided`, `open`, `analytics`, `marketing`, `labels.*`, `privacyPolicyUrl` |
| top level in `qb-product` | `options`, `hasOptions`, `isComplete` |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.