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 onceanalyticsis granted, with Consent Mode’s denieddefaultand then theupdatepushed ahead ofjsandconfig. - Meta after
marketing. The pixel is loaded only oncemarketingis granted, and is toldfbq("consent", "grant")beforeinit. - 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 In a Server Component,
getAll() so checkout.start() can also find the GA cookie (_ga_<property>, whose name is not known in advance).lib/shopkit.ts
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
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
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.Theming the banner
The default banner is a fixed card bottom-right (a sheet on phones), light or dark withprefers-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:
All custom properties and class names
All custom properties and class names
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
revisionand every stored decision reads as undecided.
Loading Google before a decision (advanced Consent Mode)
Loading Google before a decision (advanced Consent Mode)
loadBeforeConsent: true loads gtag.js or the GTM container before the shopper decides, under a Consent Mode default of everything denied. Google’s tags then send cookieless pings and model conversions.The consent store
The decision lives in aqb_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:
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.
With a third-party consent platform
If Cookiebot, CookieYes or similar already asks the shopper, hide the kit’s banner and feed it the platform’s answer: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
- React
- Web components
<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 underconsent.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.
Composing the pieces yourself
Composing the pieces yourself
<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:Analytics) is framework-free and exported from @quickbutik/kit: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.
The purchase
In redirect mode your thank-you page owns the purchase event, and the kit fires it for you: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.
Forwarding consent to the hosted checkout
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
analyticsis granted, so GA4 sees one session across both origins. On a server they need the cookie accessor’sgetAll(); 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: falsein 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:async and carries data-qb-analytics. The kit never evaluates strings.
Related
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.