Skip to main content
Every snippet on this page is plain HTML. It works unchanged with the script tag (no build step) or with @quickbutik/kit/elements behind a bundler. The pages assume the shop is configured once per page, for example:
The script tag also mounts the kit’s cookie consent banner and, once the shopper agrees, loads the shop’s own GA4, GTM and Meta pixel. See Consent and analytics.

The shell

<qb-cart-count> reads the remembered cart and never creates one, so a visitor who only browses (a crawler included) causes no write. <qb-currency-select> lists the currencies the shop offers; a choice reprices every product, list and cart on the page and is remembered. See Currencies.

Catalog

With no filter attribute it lists the catalog in the merchant’s own order. The list re-runs its query when the shopper switches currency. Price filters (min-price, max-price, sort-by="price") always run in the shop’s own currency, whatever currency the rows are shown in. A <qb-product-image> inside a row is handed that row’s product automatically.
The template is a direct child of <qb-product-list>, and the rows are inserted next to it, so the list element itself is the grid. Nesting the template inside a <ul> makes the list render nothing. See Repeats.

A category page

category-id matches direct members only; child categories are not walked. A “load more” that appends rather than replaces takes a few lines of script: see Scripting.

Product page

Naming the product

<qb-product> reflects state, empty (no such product) and complete / incomplete (whether the selection pins one variant), so a skeleton, a not-found message and a “choose a size first” hint are pure CSS. It emits qb:product-change, qb:variant-change, qb:added-to-cart, qb:add-to-cart-blocked, qb:checkout-started and qb:product-not-found.

How the picker works

  • <qb-options> repeats once per option group the product actually has, in the merchant’s order. A product with no options renders nothing and gets empty, so the same page serves simple products.
  • <qb-option-values> nested in a group row repeats once per value of that group. Each value row gets data-selected, data-available and aria-pressed.
  • Unavailable values stay clickable, on purpose. Availability is judged against the other groups, and clicking an unavailable value keeps the new choice and clears whatever contradicts it. Dim them in CSS; never write disabled into the template.
  • “Available” means the combination exists and is not hidden, not that it is in stock. After a full selection, variant.soldOut is true only when stock is tracked and exhausted.

A <select> picker

<select> may only contain <option>, so a repeat element cannot live inside one. data-qb-select-options fills a <select> you wrote and wires its change:
In a dropdown, unavailable values become disabled options, since a <select> has no way to show “dimmed but clickable”.

Swatches for one option only

option="<id or name>" on a <qb-option-values> placed outside any <qb-options> row pins it to one group, for layouts that want swatches for colour and plain buttons for everything else. It stops working the moment the merchant renames the option, and fails quietly. Prefer the generic picker above.

Add to cart and buy now

  • <qb-add-to-cart> wraps your button and keeps its disabled in step: not addable until the selection pins one variant, or while a cart mutation is in flight. It emits qb:added; the product emits qb:added-to-cart, or qb:add-to-cart-blocked with the groups still missing a choice.
  • <qb-buy-now> is add-to-cart and checkout in one: it adds the selected variant to the remembered cart (an existing basket is carried along) and goes straight to the hosted checkout. success-url, back-url, theme, no-redirect and disabled mean the same as on <qb-checkout-button>. After it navigates it stays pending until the page is restored from the back/forward cache, so a second click cannot add the item twice.
  • A plain <button data-qb-action="buy-now" data-qb-success-url="/success"> inside <qb-product> does the same without the disabled-state management.

Cart

  • Every cart element on the page shares one store, so the badge and the cart never disagree.
  • The quantity input commits on change, not per keystroke. 0 removes the line.
  • Totals come from the server and are recomputed on every read. Shipping is chosen in the hosted checkout.
  • data-qb-action="clear-cart" (or clear) deletes the remembered cart. With no cart it does nothing.

Checkout

Redirect: <qb-checkout-button>

It creates the checkout session on click and navigates the same tab to the hosted checkout (the back button returns to the cart). It is disabled while the cart is empty and guarded against a double click.
success-url must be https in production. Plain http is accepted only on localhost and loopback, so a local page works against the live shop. Any https host is accepted; there is no domain to register.

Inline: <qb-checkout>

To keep the shopper on your site, render the checkout inside the page, on a route of its own:
  • It adds one iframe served from the checkout’s own origin; payment, 3-D Secure and the receipt behave exactly as after a redirect.
  • It follows the cart. Nothing is mounted while the cart is empty (qb:checkout-empty, once). Emptying the cart from the page takes the frame down; changing it updates the checkout in place.
  • Render it on load and keep the URL parameters. Swish, Klarna, Vipps MobilePay, iDEAL and full-page 3-D Secure take the whole window and come back to this page with ?qb_checkout_session=…&qb_checkout_shop=…. The element resumes that session and removes both parameters once the frame answers. A router or redirect that strips unknown query parameters breaks this.
  • It falls back to the full-page checkout by itself on a legacy shop, when embedding is not enabled for the shop, inside a sandboxed app-builder preview, or when the frame never answers within 15 seconds (qb:checkout-fallback with detail.reason).
  • Apple Pay and Google Pay are not available inline yet.
Attributes are read once, when the session is created. Details in Embedded checkout.

Thank-you page

After payment the hosted checkout sends the shopper to <success-url origin>/success/<orderNumber>?hash=…&t=…, whatever path success-url had. The order is created asynchronously, so the page polls until it exists:
  • The session id comes from session-id, a ?session_id= parameter, or (the normal case) the session the kit remembered in a cookie when the checkout started. The storefront and the thank-you page must share an origin.
  • A direct visit with nothing remembered lands in status="unknown"; render something neutral for it.
  • confirmation.orderNumber can be empty even when completed; the number is also in the URL path, for display only.
  • On completed the kit forgets the cart and the session, so the next page load starts with an empty basket.
  • With analytics on (the default), the element fires the purchase event once the order exists, deduplicated across reloads. Add no-track-purchase to report it yourself.
timedOut is not a payment failure. The order may still land. Never show “payment failed” for it.

Static hosts need a rewrite

On a static host, /success/12345 must serve your thank-you file: Add <meta name="robots" content="noindex, nofollow"> to the cart and thank-you pages.

SEO on client-rendered pages

<qb-seo> writes the title, description, canonical, OpenGraph, Twitter card and schema.org JSON-LD into <head> from the product it sits inside:
Everything it writes is tagged data-qb-seo and removed on update, so a soft navigation leaves nothing stale. Its prices use the product’s own currency: on a shop with a display currency, a shopper browsing in it would publish a converted price that no order is charged, so server-rendered structured data in the shop’s currency is the safer source.
<qb-seo> only helps crawlers that run JavaScript and share-card scrapers. For indexable catalog and product pages, render the HTML on the server with the plain client and buildSeo(), and use the elements for the interactive parts. Never put <qb-seo> on a page whose server already writes the head: two owners of one <title> is worse than either. See SEO.

Testing the loop

Add, cart, checkout, hosted checkout, back to /success/<n>, confirmation. A shop that has not activated Quickbutik Payments runs the demo checkout: the shopper walks the real checkout, no payment is taken and no order is created, so the thank-you page is not exercised until payments are activated. See Going live.