@quickbutik/kit/elements behind a bundler. The pages assume the shop is configured once per page, for example:
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 getsempty, 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 getsdata-selected,data-availableandaria-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
disabledinto the template. - “Available” means the combination exists and is not hidden, not that it is in stock. After a full selection,
variant.soldOutis 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:
<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 itsdisabledin step: not addable until the selection pins one variant, or while a cart mutation is in flight. It emitsqb:added; the product emitsqb:added-to-cart, orqb:add-to-cart-blockedwith 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-redirectanddisabledmean the same as on<qb-checkout-button>. After it navigates it stayspendinguntil 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.0removes 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"(orclear) deletes the remembered cart. With no cart it does nothing.
Checkout
Redirect: <qb-checkout-button>
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-fallbackwithdetail.reason). - Apple Pay and Google Pay are not available inline yet.
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.orderNumbercan be empty even when completed; the number is also in the URL path, for display only.- On
completedthe 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
purchaseevent once the order exists, deduplicated across reloads. Addno-track-purchaseto report it yourself.
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:
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.
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.