Skip to main content
On by default. A storefront built with the kit shows a cookie banner, remembers the shopper’s decision, loads the merchant’s own GA4, GTM and Meta pixel (configured in the Quickbutik admin) once the shopper agrees, reports GA4-shaped commerce events, fires the purchase on the thank-you page and forwards the decision to the hosted checkout.
That is the whole integration. The consent prop only carries options: the banner’s language and privacy link. Leave it out and you still get the banner, in the language of <html lang>.

What happens by default

  • A banner on the first visit. Styled, accessible, in English, Swedish, Danish, Norwegian or Finnish, with “accept all” and “only necessary” equally prominent and a settings panel per category.
  • Nothing optional loads before the shopper agrees. No script from Google or Meta is requested while the shopper is undecided or has said no.
  • Google after analytics. gtag.js (or the GTM container) is injected once analytics is granted, with Consent Mode’s denied default and then the update pushed ahead of js and config.
  • Meta after marketing. The pixel is loaded only once marketing is granted, and is told fbq("consent", "grant") before init.
  • The merchant’s own ids. GA4, GTM and Meta ids are read from the shop (shop.tracking), the same ids the hosted checkout loads, so both report into the same properties. A shop with none configured gets the banner and sends nothing.
  • Events and the purchase are automatic. Product views, searches, cart changes and the redirect-mode purchase are reported for you. See Events.
  • The checkout is told. Every handoff to the hosted checkout, including one started on your server, carries the decision. See Forwarding consent to the hosted checkout.
  • Withdrawal stops everything. Changing your mind in the settings updates Consent Mode, revokes Meta’s consent, and no destination receives anything more.

Next.js App Router

The provider alone works in Next.js. Three small additions make a server-rendered shop exact: the banner never flashes for a returning shopper, a checkout started in a server action carries the GA session, and the thank-you page fires the purchase from the server’s snapshot.
1

Give the server client getAll()

The server client already reads cookies for the cart. Add getAll() so checkout.start() can also find the GA cookie (_ga_<property>, whose name is not known in advance).
lib/shopkit.ts
In a Server Component, set is dropped (Next.js does not allow cookie writes there), which is fine for reads.
2

Read the decision in the layout

consent.read() returns the shopper’s decision from the request, using the revision in your config. Hand it to the provider so the first HTML byte already knows whether to show the banner.
app/layout.tsx
app/providers.tsx
Pass lang explicitly when the banner renders on the server. Without it the server renders English and the browser switches to <html lang> after hydration.
3

Start the checkout as before

No consent code in the server action. checkout.start() appends the decision itself.
app/cart/actions.ts
4

Hand the thank-you page its server snapshot

app/success/[orderNumber]/order-confirmation.tsx
When the server already read the order as completed, the hook does not poll. It forgets the bought cart and fires the purchase from that snapshot, once. See The purchase.
The policy revision lives in the config (consent: { revision: 2 }), so the server read, the browser provider and the checkout handoff always agree on it.

Turning things off

Opt out in the config, not on the provider. consent: false in the config passed to createShopkitClient or <ShopkitProvider config> is read by the browser and by the server’s checkout.start() alike, and restores the behaviour from before consent existed, exactly.
<ShopkitProvider consent={false}> still works for that React tree, but is deprecated and warns: a checkout started on the server cannot see a prop, so it would still forward the decision.
A banner you place yourself always replaces the default one, and providers nested inside each other share one decision. The kit never shows two banners.

Theming the banner

The default banner is a fixed card bottom-right (a sheet on phones), light or dark with prefers-color-scheme. Its stylesheet sits in @layer qb-consent, so any rule of yours wins without !important. Map its custom properties to your own tokens:
A colour you set applies in both schemes; set it inside your own media query for a separate dark value.
Custom properties: --qb-consent-bg, -fg, -muted, -border, -accent, -accent-fg, -radius, -button-radius, -font, -font-size, -padding, -gap, -offset, -max-width, -shadow, -focus, -z-index.Class names, for anything the properties do not cover: qb-consent, qb-consent__body, __title, __description, __policy, __categories, __category, __category-name, __category-description, __actions, __button, __button--accept, --reject, --settings, --save.The stylesheet is exported as CONSENT_STYLES, with injectConsentStyles() for pages that render their own markup but want the defaults.

What is gated

  • Undecided is denied. Until the shopper chooses, nothing optional is loaded or sent, and the hosted checkout is told so.
  • Withdrawal is one call. reset() forgets the decision, shows the banner again and stops every destination. Scripts already on the page cannot be unloaded; they are simply not spoken to again.
  • A policy change asks again. Bump revision and every stored decision reads as undecided.
The decision lives in a qb_consent cookie (JSON, 180 days, SameSite=Lax, Secure on https, host-only), so a server can read it:
<ShopkitProvider> and the elements create the store for you. It is public for pages that drive consent themselves:
In React, useConsent() returns the same state and actions. Every decision is also dispatched on document as a qb:consent event. These options are accepted by <ShopkitProvider consent={{ … }}>, <ConsentProvider> and configure({ consent: { … } }). Set revision and cookieName once in the client config (consent: { revision: 2 }), where the server read and the checkout handoff see them too. If Cookiebot, CookieYes or similar already asks the shopper, hide the kit’s banner and feed it the platform’s answer:
With persist: false the kit writes no cookie, so a checkout started on the server cannot see the decision and forwards the shopper as denied. Start the checkout in the browser (useCheckout()), which reads the store, or read the platform’s own cookie on the server and pass its answer to appendCheckoutHandoffParams, which replaces the forwarded decision.

Building your own banner

Your <ConsentBanner> anywhere under the provider replaces the default one. The same render function can also go straight on the provider: consent={{ banner: (state) => … }}.<ConsentSettingsButton> renders a <button type="button"> labelled in the provider’s language (“Cookie-inställningar” in Swedish) that reopens the settings panel. It renders nothing when consent is off, so a shared footer is safe.

Analytics

Analytics comes with the provider. Its options go under consent.analytics:
The same options work with configure({ analytics: { … } }) and, for the common ones, on the script tag (data-analytics="false", data-analytics-require-consent="false", data-analytics-load-before-consent). Events a destination cannot receive yet (consent pending, the shop’s ids still loading) are remembered and delivered when it can, so a view_item fired before the shop answers still reaches GA4. A denial drops what was waiting. One decision per page, one hub per shop. A <ShopkitProvider> nested inside another (a campaign provider inside the site’s) reuses the outer consent store, banner and analytics hub, and reports its own cart into the shared hub. A nested provider for a different shop gets its own hub under the same decision.
<ConsentProvider>, <AnalyticsProvider> and <ConsentBanner> stay public. The shop provider reuses a <ConsentProvider> above it, and an <AnalyticsProvider> inside it joins the shop provider’s hub, so nothing is created twice:
The hub itself (Analytics) is framework-free and exported from @quickbutik/kit:
Without a consent store the hub fails closed: it treats the shopper as undecided forever and warns once.

Events

GA4’s vocabulary, money in major units, item_id the bare numeric product id, the variant as item_variant: The Google destination forwards events with gtag (GA4 or Ads id) or dataLayer.push (GTM container only), never both. The Meta destination translates to ViewContent, Search, AddToCart, InitiateCheckout, AddPaymentInfo, ViewCategory and Purchase. The kit does not fire begin_checkout when the shopper is redirected: the hosted checkout fires it with the same ids, and both would count every checkout twice.
A single-page app that reports its own navigations passes sendPageView: false to the Google destination and calls analytics.trackPageView() on route change, so the first page is not counted twice.

The purchase

In redirect mode your thank-you page owns the purchase event, and the kit fires it for you:
The payload comes from the checkout session’s own lines and total, and is deduplicated across reloads with the platform’s key (qb_purchase_tracked_{storeId}_{orderNumber}). The Meta Purchase carries eventID purchase_{storeId}_{orderNumber}, the id the platform’s server-side events use, so Meta merges rather than double-counts. Not fired in inline success mode (the checkout’s confirmation fires it) or on a legacy-checkout shop (no session to read); call useAnalytics().trackPurchase(purchase, { orderNumber }) there. useTrackPurchase({ orderNumber }) fires it by hand from an order number you already have. The embedded checkout’s purchase reaches your page as an event and the hub forwards it. Every handoff the kit performs (useCheckout(), <qb-checkout-button>, <qb-buy-now>, and checkout.start() on a server client) appends the decision to the checkout URL:
  • The checkout sets Consent Mode from it before its own tags load.
  • An undecided shopper is forwarded as consentCategories= (nothing granted).
  • The GA ids are forwarded only when analytics is granted, so GA4 sees one session across both origins. On a server they need the cookie accessor’s getAll(); without it the decision is still forwarded.
  • A URL decorated on the server and again in the browser is never stacked.
  • Nothing is appended with consent: false in the config.
appendCheckoutHandoffParams(url, state, { cookies }) is exported for a handoff the kit does not perform.

Your own destination

requires names the category a destination needs before it receives anything, load() included. key (optional) tells “the same destination again” from “a different one” when destinations is an inline array that is recreated on every render: the same key is kept and nothing is replayed into it, a new key is swapped in. Without a key the object itself is the identity, so give one to a destination you create inline. The built-ins set it from their ids (google:G-X|GTM-Y|AW-Z, meta:123); changing another option on the same ids needs a remount. googleTagDestination(options) and metaPixelDestination({ pixelId }) are exported for ids that are not the shop’s.

Content Security Policy

With analytics on, allow the merchant’s vendors:
Every injected tag is async and carries data-qb-analytics. The kit never evaluates strings.

Checkout

Redirect and inline modes, the thank-you page and order confirmation.

Storage and SSR

Cookie accessors for server clients and rendering without a flash.