Skip to main content

Fast script loading, shadcn registry and new integrations

Minor releaseNo breaking changes

Start allowed scripts before hydration, add shadcn consent UI and 18 languages, and connect more analytics, marketing and support tools.

In c15t 3.2, consent no longer makes your scripts late. Scripts that a visitor already allowed now start before the page hydrates. This release also adds a shadcn registry, 18 languages, and integrations for HubSpot, FullStory, Amazon Ads, and more.

Fast script loading

When a visitor already gave consent, c15t starts their allowed scripts from the server HTML, before the page hydrates. This works in Next.js, TanStack Start, Nuxt, and SvelteKit. See Loading scripts from the server for the benchmarks and implementation details.

Vendor helpers in @c15t/integrations that support early starts run before hydration. Other helpers load after hydration. Nothing starts early while the policy asks the visitor to choose, or when the page comes from prerendered or cached HTML. To turn the early start off, set scriptLoader: { serverStart: false }. See Start allowed scripts before hydration.

Browser manifest mode also does less work. With manifest({ resolve: 'browser' }), c15t skips the /manifest request when the server state already holds the visitor's policy. It also skips the policy resolver, which is about 20 KB gzipped.

shadcn registry

Your consent banner can now use the same components as the rest of your app. Add it with the shadcn CLI:

npx shadcn add @c15t/next-consent-manager

Each item renders your project's own shadcn components, and the markup is yours to change. c15t keeps the consent logic, so you still get its fixes when you update c15t.

Follow the Next.js shadcn guide to connect your Inth project and add the components to your app.

ItemWhat it gives you
consent-managerThe provider setup for your framework
consent-bannerThe banner, also as a consent wall that blocks the page
consent-dialog, consent-sheetThe preference center in a dialog or a sheet
consent-preferencesThe category and vendor switches
consent-dialog-triggerA button that floats on the page and opens the preference center again
consent-widgetThe preference center inline on a page
consent-gateA gate that holds an embed until the visitor allows its category
consent-brandingThe "Secured by" link and the trigger mark

Each item has a next-, react-, and tanstack-start- version. The registry has a build for all 27 shadcn styles, so Radix, Base UI, and React Aria projects all work. It has no IAB TCF items yet. Under an IAB policy, the TCF banner from <IABProvider> shows instead of the shadcn banner.

Build your own banner

The registry items use public headless hooks, so a banner in any design system can use them too. useBannerCopy() and useConsentLabels() return the same copy and button labels as the stock banner, in the active language. useConsentGate(), useScrollLock(), useConsentTextDirection(), and StreamedConsentSurface cover the rest of what the stock UI does. See headless UI.

Vue, Svelte, and the script tag get the same copy as plain functions: resolvePromptCopy, resolveActionLabels, and resolvePreferenceControlLabels.

18 new languages

More of your visitors can now read the banner in their own language. c15t ships consent copy in 53 languages, up from 35 in 3.1.

  • Arabic (ar), Urdu (ur), and Yiddish (yi), which read right to left
  • Bangla (bn), Haitian Creole (ht), Korean (ko), and Russian (ru)
  • Japanese (ja), Thai (th), Malay (ms), Vietnamese (vi), and Turkish (tr)
  • Tamil (ta), Telugu (te), Marathi (mr), Kannada (kn), Malayalam (ml), and Punjabi in Gurmukhi script (pa)

With these, c15t covers all 12 languages that New York State's language access law names for state agencies.

manifest() loads each language on demand, and the backend serves them from /init. To bundle them, import one with @c15t/translations/<code> or all of them with @c15t/translations/all. See translations.

Banners, dialogs, and preference centers also read right to left for Central Kurdish (ckb), Uyghur (ug), Syriac (syr), and N'Ko (nqo). A script subtag now sets the direction, so ku-Arab reads right to left.

Integrations

New consent-gated helpers in @c15t/integrations cover:

Default has separate Pixel and Forms SDK helpers. Choose one per page.

Databuddy now waits for measurement consent and supports optional session replay and tracking feature switches. See the Databuddy guide.

Shopify and Google Tag Manager

  • Shopify. createShopifyCustomerPrivacy sends each c15t choice to Shopify's Customer Privacy API. Hydrogen holds analytics events until the visitor answers the c15t banner. See the Shopify guide.
  • Google Tag Manager. A community template loads the c15t script tag from GTM and sends each choice to Consent Mode. See the Google Tag Manager guide.

A subject ID from the first page load

snapshot.subject.subjectId now exists before the visitor makes a choice. The first save keeps that ID, and clearing choices or revoking consent keeps it too. The ID alone grants no consent, so the banner still shows.

Next.js, TanStack Start, Nuxt, Astro, and SvelteKit can set the c15t-sid cookie on the first request, so the server and browser start with the same ID. Other servers can use resolveRequestSubject() from c15t/server. See subject IDs.

identify() now works before the first consent save. The backend creates the subject and links the identity in one transaction. Acknowledging a notice that offers only necessary categories also keeps the visitor's ID across reloads.

Keep fixed elements still while scrolling is locked

When a blocking banner or preference dialog needs scrollbar padding, c15t now exposes that width as --c15t-scroll-lock-gap. Use it in the right offset or padding of fixed headers and chat buttons to keep them in place. The padding also accounts for CSS zoom. See scroll lock styling.

Svelte and SvelteKit through c15t

Install c15t and import from c15t/svelte, as you do for the other frameworks. SvelteKit helpers live at c15t/svelte/kit, server resolution at c15t/svelte/server, and the Vite plugin at c15t/svelte/vite. c15t setup writes these imports for you. See the SvelteKit quickstart.

offline() is now local()

We renamed offline() to local() to make clear that choices stay in the browser. Development builds now show the limits once per page:

[c15t] Local mode keeps consent only in this browser. c15t keeps no
server-side record of consent. If you rely on consent under GDPR, Article
7(1) requires you to demonstrate it. Keep your own consent records or use
a c15t backend. Local mode also has no geolocation, so every visitor gets
the same policy. Use it for development and tests. For production, see
https://c15t.com/docs/concepts/modes
Set acknowledgeNoRecords: true to hide this message.

offline() still works until v4. See local mode.

Clearer backend errors

When the backend refuses a request, the error now says why. In 3.1, an error over HTTP/2 could stop at /init responded 402. Now it includes the backend's message and code, with status and code properties to match.

After a 402 response, c15t no longer sends the consent save again. The choice stays in the browser.

The backend now records consent only for sites in trustedOrigins. A save naming any other site gets 400 DOMAIN_NOT_ALLOWED and writes nothing. In 3.1, any site could create a row in the domain table. Only an entry's host counts, so localhost:3000 covers localhost, and * still covers every site. Without trustedOrigins, saves are not checked.

identityToken.requireVerifiedLinks is a new option, off by default. It stores a link to your user ID only when an API key or identity token proves it. Without proof, identify() gets 401 IDENTITY_TOKEN_REQUIRED, and a consent save is recorded without the user ID.

See origins, identity and secrets and verify identity links.

Fixes

  • A preference center that a visitor opens before the backend answers now stays open when the answer arrives.
  • Under an IAB policy, c15t now creates the IAB CMP once per page load. In 3.1, it could create the CMP up to 100 times.
  • The backend now records the tenant on identify_user audit entries.
  • The script-tag banner and DevTools panel stay visible on pages whose CSS hides empty elements, including Shopify's Dawn theme.
  • The c15t tab now supports Nuxt DevTools 4, including popup windows. See Nuxt DevTools.
  • The dialog close-button offset and small and large trigger labels now scale with the visitor's text-size setting.
  • The backend now accepts consent timestamps up to 10 minutes ahead of its clock and stores them unchanged. A slightly fast visitor clock no longer causes a consent save to fail.
  • Invalid identity links return a validation error before the backend writes anything. Retrying an unchanged link no longer adds duplicate audit entries.

Before you upgrade

Update c15t and any @c15t/* packages you use to 3.2 together.

If you set trustedOrigins on a self-hosted backend, make sure it lists every site that saves consent, including the site the backend is mounted on. In 3.1, a same-origin save worked without it. In 3.2, a save from a site not on the list gets 400 DOMAIN_NOT_ALLOWED. c15t keeps a refused save and retries it up to 10 times over 7 days, so a site you add in that time still gets its consent recorded.

If your cookie policy lists c15t cookies, add c15t-sid and c15t-notice-subject. Their prefixes follow your custom storageKey.

If you use the Next.js proxy or SvelteKit handle, set subjectCookie: false for routes whose responses enter a shared cache. Also disable server subject cookies when browser persistence is off or a consentSource owns consent. Pass the same storage settings to the server helper and browser. See the Next.js subject cookie setup and SvelteKit request setup.

If your own code calls POST /subjects or PATCH /subjects/:id, send Content-Type: application/json. Both routes now answer other body types with 415 UNSUPPORTED_MEDIA_TYPE, as 2.x did. A save's domain must also be 1 to 253 characters with no control characters. The c15t clients and @c15t/node-sdk already meet both rules.

Two changes can move things on a page:

  • The banner now stays in its corner for right-to-left languages. In 3.1, a bottom-left banner moved to the bottom right for Hebrew. To put the banner on the right for right-to-left pages, set position.
  • Plain ku now reads left to right, because it usually means Kurmanji. For Sorani, use ckb or ku-Arab.