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

# Web components

> Every qb-* element: attributes, properties, scope paths, reflected state and events, plus configure() and defineElements().

<Info>
  This page is the curated reference. The complete, generated type reference lives at [/kit/api/elements](/kit/api/elements) and is regenerated from the published typings. For a walkthrough, start with [Web components setup](/kit/web-components/setup).
</Info>

```ts theme={null}
import { configure, defineElements } from "@quickbutik/kit/elements"

configure({ publishableKey: "qb_pk_…", locale: "sv-SE" })
defineElements()
```

The elements are **light DOM with no markup of their own**: no shadow root, no wrapper elements, no class names. They bind data into the markup you write and reflect their state as attributes for your CSS. The exceptions: `<qb-product-image>` renders one `<img>`, `<qb-cart-count>` writes its number, `<qb-seo>` writes into `<head>`, `<qb-checkout>` renders the checkout's `<iframe>`, and `<qb-currency-select>`, `<qb-consent-banner>` and `<qb-consent-settings>` render a default `<select>`, dialog or button only when you give them no markup of your own.

<Warning>
  `@quickbutik/kit/elements` is **browser only**. Importing it in Node throws `ReferenceError: HTMLElement is not defined`. Import it from client code (an Astro `<script>`, a Vue `onMounted`, a Svelte `onMount`).
</Warning>

## Setup functions

### configure

```ts theme={null}
configure(options: ConfigureOptions): { client: ShopkitClient; cartStore: CartStore; currency?: string; locale?: string; analytics?: Analytics | null }
```

Builds the page's **ambient shop**: one client and one shared cart store that every element finds on its own. By default it also sets up cookie consent (a default banner appended to `<body>` unless the page has its own `<qb-consent-banner>`) and the shop's own analytics gated on it. Takes every [`ShopkitConfig`](/kit/reference/client) option (except that `consent` takes the richer shape below) plus:

<ParamField body="currency" type="string">The currency the page **browses in** until the shopper picks one: sent on every product, cart and checkout request. Leave it out to start in the shop's own currency. Prices format with the currency each response states (`product.currency`, `cart.currency`); this value is only the formatting fallback for a platform older than those fields. See [Currencies](/kit/concepts/currencies).</ParamField>
<ParamField body="locale" type="string">BCP 47, for number formatting. Defaults to the browser's.</ParamField>

<ParamField body="consent" type="boolean | ElementsConsentOptions | ConsentStore" default="true">
  Cookie consent for the page, **on by default**. `false` turns it all off: no banner, no analytics, nothing appended to checkout URLs. A `ConsentStore` you already hold is used as is.

  <Expandable title="ElementsConsentOptions">
    <ParamField body="banner" type="boolean" default="true">Mount the default `<qb-consent-banner>` at the end of `<body>`. A banner the page places itself always replaces it.</ParamField>
    <ParamField body="lang" type="string">Language of the banner's built-in copy (`en`, `sv`, `da`, `nb`, `fi`). Defaults to `<html lang>`.</ParamField>
    <ParamField body="privacyPolicyUrl" type="string">Adds a "Privacy policy" link to the banner.</ParamField>
    <ParamField body="unstyled" type="boolean">Leave out the banner's stylesheet.</ParamField>
    <ParamField body="revision / cookieName / maxAgeDays / domain / sameSite / secure / persist / initialState" type="ConsentStoreOptions">The page's consent store. Bump `revision` to ask every shopper again.</ParamField>
  </Expandable>
</ParamField>

<ParamField body="analytics" type="boolean | ElementsAnalyticsOptions" default="true">
  The merchant's own GA4, GTM and Meta pixel from the admin, gated on consent: nothing from Google or Meta loads before the shopper agrees (unless `loadBeforeConsent`). `false` keeps the banner and loads no pixel.

  <Expandable title="ElementsAnalyticsOptions">
    <ParamField body="auto" type="boolean" default="true">Load the destinations from `shop.tracking`. `false` for a page that passes only its own `destinations`.</ParamField>
    <ParamField body="destinations" type="AnalyticsDestination[]">Destinations of your own.</ParamField>
    <ParamField body="requireConsent" type="boolean" default="true">`false` runs analytics ungated, and the hosted checkout too.</ParamField>
    <ParamField body="loadBeforeConsent" type="boolean" default="false">Load GTM or gtag.js before a decision, in Consent Mode denied. A GTM container loaded this way runs all of its tags.</ParamField>
    <ParamField body="currency / storeId / bufferSize / debug" type="AnalyticsOptions">See [`AnalyticsProvider`](/kit/reference/react#analyticsprovider).</ParamField>
  </Expandable>
</ParamField>

Call it **once at startup**. A second call replaces the ambient shop for elements mounted afterwards, while elements already on the page keep the first one.

### defineElements

```ts theme={null}
defineElements(options?: { prefix?: string; warnOnUnhandledActions?: boolean }): { prefix: string; tags: Record<string, string> }
```

Registers the tags. Explicit rather than an import side effect, so the entry stays tree-shakeable. Idempotent. `prefix: "shop-"` gives `<shop-product>` for a name collision; `warnOnUnhandledActions: false` silences the typo warning.

### Other exports

| Export | Does |
| - | - |
| `setCurrency(code)` | Switch the ambient shop's currency and remember it. Every `qb-product`, `qb-product-list` and cart element reprices. `null` returns to the default. The markup twin is `<qb-currency-select>` |
| `getAmbientShop()` | The ambient `{ client, cartStore, currency, locale, analytics }`, or `null` |
| `getAmbientConsent()` / `requireAmbientConsent()` | The page's consent store, or `null` / created with defaults |
| `hasShop()` | Whether an ambient shop exists |
| `setAmbientClient(client)` | Use a client you built as the ambient shop |
| `resetAmbientShop()` | Forget the ambient shop (tests) |
| `elementConstructors` | Every element class by tag, for registering a subset by hand |
| `stopActionWarnings()` | Turn off the unhandled-action warning |
| `QbElement`, `QbScopeElement` | Base classes for an element of your own that joins the same scope chain |
| `createContext`, `provideContext`, `requestContext`, `ContextRequestEvent` | The W3C `context-request` protocol the elements use; interoperates with Lit's `@lit/context` |

Every element class is exported too: `QbShopElement`, `QbProductElement`, `QbOptionsElement`, `QbOptionValuesElement`, `QbAddToCartElement`, `QbBuyNowElement`, `QbProductListElement`, `QbProductImageElement`, `QbCartElement`, `QbCartItemsElement`, `QbCartCountElement`, `QbCheckoutButtonElement`, `QbCheckoutElement`, `QbOrderConfirmationElement`, `QbSeoElement`, `QbCurrencySelectElement`, `QbConsentBannerElement`, `QbConsentSettingsElement`, `QbConsentGateElement`.

## Bindings

Put these on any element inside a component. Values are **paths** into the component's scope (`product.images.0.url`), never expressions. A missing branch renders empty instead of throwing.

| 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` or `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="…"` | Toggles `hidden`. Write `hidden` in the markup too, or it flashes before the first bind |
| `data-qb-value="item.quantity"` | Sets an input's `value`, only when it differs |
| `data-qb-select-options` | Fills a `<select>` you wrote with the enclosing group's values and wires its `change`. Your placeholder `<option value="">` is kept |

Bindings written on a scope-owning element itself (`qb-product`, `qb-product-list`, `qb-options`, `qb-option-values`, `qb-cart`, `qb-cart-items`, `qb-order-confirmation`) are never applied; each binds only what is inside it.

## Repeats

`qb-product-list`, `qb-options`, `qb-option-values` and `qb-cart-items` clone their `<template>` once per item.

* The `<template>` must be a **direct child** of the repeating element. Nested in a `<ul>` or `<div>`, it is never found and the list renders nothing.
* Rows are inserted after the template as siblings, so the repeating element is the list container: style it as the grid.
* Rows are keyed and reused, so a quantity input keeps its caret while its line re-renders.

## Actions

`data-qb-action` on any element, resolved by the nearest enclosing component that knows the verb. `click` and `change` both dispatch.

| 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 to the 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 row's currency |
| `accept-all`, `reject-all`, `save`, `open-settings`, `close-settings` | `qb-consent-banner` | The consent decision. `save` reads every `input[data-qb-consent]` inside the banner |

Quantity for an add, in order: `data-qb-quantity="3"` on the clicked element, an `input[data-qb-quantity-input]` inside the wrapper, the wrapper's `quantity` attribute, then 1. In a cart row, `input[data-qb-quantity]` (no suffix) is the bound quantity of the line and commits on `change`.

## Events

Every element emits `qb:`-prefixed `CustomEvent`s that bubble and compose, so one listener on `document` catches them all.

```js theme={null}
addEventListener("qb:added-to-cart", (event) => openDrawer(event.detail.cart))
```

## qb-shop

Optional. The ambient shop from `configure()` or the script tag covers a normal page. Use `<qb-shop>` for two shops on one page, one section against a different API, one section selling into a campaign storefront, or a client you built yourself. A `<qb-shop>` ancestor wins for its subtree.

| | |
| - | - |
| Attributes | `publishable-key`, `api-url`, `checkout-url`, `image-base-url`, `currency`, `locale`, `storage-key-prefix`, `storefront-id`, `consent` (`"false"` to turn consent off for the subtree), `analytics` (`"false"` for no pixels), `privacy-policy-url` |
| Properties | `client` (wins over every attribute), `initialCart` (write-only, a cart read on the server), `cartStore` (read-only) |
| Reflects | `state` |
| Events | `qb:shop-ready` with `{ client, cartStore, currency, locale, analytics }` |

Changing `storefront-id` builds a new client and cart store. `currency` is the currency the subtree browses in by default; changing the attribute later switches that client with `setCurrency()` without rebuilding it or its cart. A `<qb-shop>` for the same shop as the page reuses the page's analytics hub.

## qb-product

Resolves one product and holds its variant selection. React twin: `<ProductProvider>`.

| | |
| - | - |
| Attributes | `slug`, `product-id`, `variant-id`, `select-first-available`, `currency`, `locale` |
| Properties | `product` (get/set; wins over the attributes), `selection` (the live `ProductController`) |
| Methods | `addToCart(quantity?)`, `buyNow(input, quantity?)`, `canAddToCart()` |
| Scope | `product.*` (the `Product` plus `product.image`), `price.*`, `variant.*`, `options`, `hasOptions`, `isComplete` |
| Reflects | `state` (`idle`, `loading`, `ready`, `error`), `empty`, `complete`, `incomplete`, `buying` |
| Events | `product-change` `{ product }`, `variant-change` `{ variant }`, `added-to-cart` `{ cart, quantity }`, `add-to-cart-blocked` `{ missingOptions }`, `checkout-started`, `product-not-found` `{ slug, productId }`, `error` |

`slug` walks the catalog; on a large catalog resolve the product server-side and assign `el.product`.

The product is fetched in the client's currency and **reloads when it changes**; a product assigned as `el.product` is re-read by its id. The `currency` attribute is only a formatting fallback for a product that does not state its own `product.currency`. With analytics on, it tracks `view_item` once per product.

<ResponseField name="price.*" type="PriceView">
  <Expandable title="properties">
    <ResponseField name="amount" type="number | null">Minor units; null until a variant is pinned.</ResponseField>

    <ResponseField name="formatted" type="string" />

    <ResponseField name="display" type="string">The exact figure once pinned, the range before that. The one to render.</ResponseField>

    <ResponseField name="currency" type="string | null" />

    <ResponseField name="compareAt" type="{ amount, formatted, currency }" />

    <ResponseField name="onSale" type="boolean" />

    <ResponseField name="min / max" type="{ amount, formatted, currency }" />

    <ResponseField name="isRange" type="boolean" />
  </Expandable>
</ResponseField>

<ResponseField name="variant.*" type="VariantView">
  <Expandable title="properties">
    <ResponseField name="id" type="number | null" />

    <ResponseField name="sku" type="string | null" />

    <ResponseField name="stock" type="number | null" />

    <ResponseField name="soldOut" type="boolean">Only when stock is tracked and exhausted.</ResponseField>

    <ResponseField name="preorder" type="boolean" />

    <ResponseField name="selected" type="boolean" />
  </Expandable>
</ResponseField>

## qb-options

Repeats its `<template>` once per option group **the product actually has**, in display order. No attributes. A product with no options renders nothing.

| | |
| - | - |
| Row scope | `group.id`, `group.name`, `group.position`, `group.values`, `group.selectedValueId`, `group.chosen` |
| Reflects | `empty`; per row `data-option-id`, `data-chosen` |

## qb-option-values

Nested in a `qb-options` row, repeats once per value of that group.

| | |
| - | - |
| Attributes | `option` (an id or name; pins it to one group when used outside `qb-options`) |
| Row scope | `value.id`, `value.name`, `value.optionId`, `value.position`, `value.selected`, `value.available`, `value.unavailable`, `value.variantIds`, plus the enclosing `group.*` |
| Reflects | `empty`; per row `data-value-id`, `data-selected`, `data-available`, and `aria-pressed` when the row root is a `<button>` or has a `role` |
| Actions | `select` |

Keep unavailable values clickable; dim them with `[data-available="false"]`. Clicking one keeps the new choice and clears what contradicts it.

## qb-add-to-cart

Wraps your button inside a `qb-product` and keeps its `disabled` in step: not addable until the selection pins one variant, or while a mutation is in flight.

| | |
| - | - |
| Attributes | `quantity`, `disabled` |
| Methods | `add(quantity?)` |
| Reflects | `pending`, `blocked`, `state`; inner buttons' `disabled` |
| Events | `added` `{ cart, quantity }` |

## qb-buy-now

"Buy now": adds the selected variant to the remembered cart and goes to the hosted checkout. Sits inside a `qb-product`.

| | |
| - | - |
| Attributes | `success-url`, `back-url`, `theme`, `quantity`, `disabled`, `no-redirect` |
| Methods | `buy(quantity?)` |
| Reflects | `pending`, `blocked`, `state` |
| Events | `checkout-started` (the start result), `error` |

After it navigates it stays `pending` until the page is restored from the back/forward cache.

## qb-product-list

Repeats a product card. With no filter attribute it lists the catalog in the merchant's order; any of `search`, `category-id`, `sort-by`, `min-price`, `max-price` switches to search. Changing an attribute aborts the previous request.

| | |
| - | - |
| Attributes | `limit`, `cursor`, `search`, `category-id`, `sort-by`, `sort-order`, `min-price`, `max-price`, `href-template` (`:slug`, `:id`), `currency`, `locale` |
| Properties | `products` (read-only) |
| Methods | `reload()` |
| Row scope | `product.*` plus `product.image`, `product.priceFormatted`, `product.href` |
| Scope outside rows | `list.loading`, `list.error`, `list.count`, `list.empty`, `list.hasMore`, `list.nextCursor` |
| Reflects | `state`, `empty`; per row `data-product-id` |
| Events | `products` `{ products }` |

A `qb-product-image` inside a row is handed that row's product. Setting `cursor` replaces the page; it does not append. The list re-runs its query when the client's currency changes, and `product.priceFormatted` uses each product's own `product.currency`. With `search` set and analytics on, it tracks `search` once per term.

## qb-product-image

Renders one `<img>` with the CDN URL resolved, reused across updates so a variant swap does not flash. Finds its product from an enclosing `qb-product` or list row.

| | |
| - | - |
| Attributes | `index`, `image-id`, `width`, `height`, `alt`, `sizes`, `widths`, `densities`, `quality`, `format`, `fit`, `priority`, `include-pending`, `img-class`, `img-style`, `img-sizes` |
| Properties | `product`, `image` |
| Reflects | `empty` when there is no image |

## qb-cart

Scope for a cart view. Shares the page's single cart store. Loaded on first mount and never created speculatively. Re-reads the same cart when the client's currency changes; formatted figures use `cart.currency`.

| | |
| - | - |
| Attributes | `currency`, `locale` |
| Scope | `cart.*` (the `Cart` plus `empty`, `subtotalFormatted`, `totalFormatted`, `totalTaxFormatted`, `totalDiscountFormatted`), `pending` |
| Reflects | `state`, `empty`, `pending` |
| Actions | `clear` / `clear-cart`, `refresh` |

## qb-cart-items

Repeats the cart lines.

| | |
| - | - |
| Attributes | `currency`, `locale` |
| Row scope | `item.*` (the `CartItem` plus `unitPriceFormatted`, `lineTotalFormatted`, `compareAtFormatted`, `discountFormatted`, `onSale`) and `cart.*` |
| Reflects | `state`, `empty`, `pending`; per row `data-item-id`, `data-available` |
| Actions | `remove`, `increment`, `decrement` |

## qb-cart-count

Writes the cart's item count as its own text. No attributes. Reflects `zero` and `state`.

## qb-checkout-button

Wraps your button. Starts the session on click and navigates with `location.assign`. Disabled while the cart is empty; guarded against a double click.

| | |
| - | - |
| Attributes | `success-url`, `back-url` (relative values resolve against the page), `theme`, `disabled`, `no-redirect` |
| Methods | `checkout()` |
| Reflects | `empty`, `blocked`, `pending`, `state`; inner buttons' `disabled` |
| Events | `checkout-started` (the full start result: `url`, `checkout`, `handoffId`, `cartId`, `session` or `legacyOrderUuid`), `checkout-empty`, `error` |

`no-redirect` stops the navigation so you can use `event.detail.url` yourself. There is no `language` attribute; the checkout uses the shop's language unless you start it yourself with `checkout.start({ language })`.

## qb-checkout

The hosted checkout rendered inline in an iframe. It follows the cart, resumes the return leg of redirect payment methods from `?qb_checkout_session=` and `?qb_checkout_shop=`, and falls back to the redirect when a shop or page cannot embed. See [Embedded checkout](/kit/concepts/embedded-checkout).

| | |
| - | - |
| Attributes | `success-url`, `back-url`, `lang`, `theme`, `confirmation` (`redirect` or `inline`), `min-height` (default 600) |
| Properties | `checkout` (the `EmbeddedCheckout` handle, or `null`) |
| Reflects | `state` |
| Events | `checkout-ready`, `checkout-step`, `checkout-event`, `checkout-complete`, `checkout-demo-complete`, `checkout-error`, `checkout-fallback`, `checkout-empty` |

The `detail` of each `checkout-*` event is the matching [`EmbeddedCheckout` event payload](/kit/reference/checkout). Attributes are read once, when the session is created.

## qb-order-confirmation

Polls the order confirmation on the thank-you page. The session id comes from `session-id`, a `?session_id=` query parameter, or the session the kit remembered when the checkout started (the normal case).

| | |
| - | - |
| Attributes | `session-id`, `no-track-purchase` (opt out of reporting the `purchase` event) |
| Scope | `confirmation.status`, `.kind`, `.orderNumber`, `.loading`, `.completed`, `.failed`, `.timedOut`, `.processing`, `.error`, `.raw` |
| Reflects | `status` (`pending`, `completed`, `failed`, `timeout`, `unknown`), `state` |
| Events | `confirmation` (the `ConfirmationOutcome`), `error` |

`timedOut` is **not** a payment failure. A direct visit with nothing remembered lands in `status="unknown"`; render a neutral page for it.

With analytics on, a completed order is reported as the `purchase` event once, deduplicated across reloads.

## qb-currency-select

The currency switcher. Reads the shop's currencies (`shop.get()`, so the key needs `checkout:read`) and switches the client with `setCurrency()` on a choice. The choice is remembered, and every `qb-product`, `qb-product-list` and cart element on the page reprices (the cart elements by re-reading the same cart). A switch made anywhere else is reflected too. React twin: `useCurrency()`.

```html theme={null}
<!-- Empty: a native <select> of the shop's currencies is added -->
<qb-currency-select></qb-currency-select>

<!-- Your own <select>: filled (a value="" placeholder is kept), or left alone when you wrote the <option>s -->
<qb-currency-select label="code-name">
  <label>Currency <select></select></label>
</qb-currency-select>

<!-- A template: one clone per currency -->
<qb-currency-select>
  <template>
    <button type="button" data-qb-action="set-currency" data-qb-text="option.code"></button>
  </template>
</qb-currency-select>
```

| | |
| - | - |
| Attributes | `label` (option text: `code`, the default, gives `EUR`; `name` gives `euro` in `locale`; `code-name`), `locale` |
| Scope | `currency.code` (what prices are shown in), `currency.base`, `currency.mode` (`base`, `display`, `charge`), `currency.chargeCurrency`, `currency.rate`, `currency.count` |
| Row scope | `option.code`, `option.label`, `option.name`, `option.mode`, `option.rate`, `option.selected`, `option.base` |
| Reflects | `state`, `data-currency`, `data-mode`, `empty` (the shop offers only its own currency); per row `data-currency`, `data-selected`, and `aria-pressed` on a `<button>` |
| Actions | `set-currency` |
| Events | `currency-change` `{ currency }` |

The added `<select>` is the one piece of markup it writes; no `<select>` is generated next to static `set-currency` buttons. Hide it for a single-currency shop with `qb-currency-select[empty] { display: none; }`. See [Currencies](/kit/concepts/currencies).

## qb-consent-banner

The cookie banner. Left empty it renders the kit's default accessible, styled dialog; with your own markup inside, it binds your markup to the consent store. A page configured by `configure()`, `<qb-shop>` or the script tag gets a default banner appended to `<body>` automatically, unless it places its own.

```html theme={null}
<qb-consent-banner>
  <p data-qb-text="consent.labels.description"></p>
  <fieldset data-qb-show="consent.open">
    <label><input type="checkbox" data-qb-consent="analytics"> Analytics</label>
    <label><input type="checkbox" data-qb-consent="marketing"> Marketing</label>
  </fieldset>
  <button data-qb-action="reject-all">Only necessary</button>
  <button data-qb-action="open-settings" data-qb-hide="consent.open">Settings</button>
  <button data-qb-action="save" data-qb-show="consent.open">Save</button>
  <button data-qb-action="accept-all">Accept all</button>
</qb-consent-banner>
```

| | |
| - | - |
| Attributes | `lang`, `privacy-policy-url`, `show-when-decided`, `unstyled` |
| Scope | `consent.status`, `consent.undecided`, `consent.decided`, `consent.open`, `consent.analytics`, `consent.marketing`, `consent.labels.*`, `consent.privacyPolicyUrl` |
| Reflects | `status` (`undecided`, `decided`), `open`, and `hidden` once decided (unless `show-when-decided`) |
| Actions | `accept-all`, `reject-all`, `save`, `open-settings`, `close-settings` |

Every decision writes the `qb_consent` cookie and dispatches `qb:consent` on `document` with the state as `detail`.

## qb-consent-settings

A "Cookie settings" control for a footer: reopens the banner's preferences. Left empty it renders a `<button type="button">` labelled in `lang`; or wrap your own link or button. Hidden when the page turned consent off.

| | |
| - | - |
| Attributes | `lang` |
| Reflects | `status` (`undecided`, `decided`) |

## qb-consent-gate

Content that waits for consent: an embedded video, a chat widget.

```html theme={null}
<qb-consent-gate category="marketing">
  <template><iframe src="https://www.youtube.com/embed/…"></iframe></template>
  <p>Allow marketing cookies to see the video.</p>
</qb-consent-gate>
```

The direct `<template>` child is the gated content and is cloned in only once `category` is granted; every other child is the placeholder shown until then.

| | |
| - | - |
| Attributes | `category` (`analytics` or `marketing`) |

## qb-seo

Writes the title, description, canonical, OpenGraph, Twitter card and JSON-LD into `<head>` from the product it sits inside. Everything it writes carries `data-qb-seo` and is removed on update. **Only for client-rendered pages**; a server-rendered page should use `buildSeo()`.

| | |
| - | - |
| Attributes | `base-url`, `site-name`, `title`, `title-template`, `description`, `url`, `locale`, `currency`, `product-path`, `category-path`, `default-image`, `twitter-site`, `twitter-creator`, `no-index`, `no-follow`, `organization`, `skip-title`, `skip-json-ld` |
| Properties | `tags` (the resolved `SeoTags`) |
| Events | `seo` (the `SeoTags`) |

## Errors

Errors are mostly silent by design. A failed product lookup sets `state="error"` on `qb-product`. A failed cart mutation is captured by the store: the add resolves `null`, no success event fires, the cart elements reflect `state="error"`, and the message is in `cartStore.getSnapshot().error`. `qb:error` with `{ error, context }` fires for startup failures (no shop, a bad key), a checkout start that fails and a confirmation that cannot run.


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