Skip to content

Blog

Engineering

c15t 3.2: allowed scripts start before hydration

Allowed scripts now start before the page hydrates. c15t 3.2 also adds a shadcn registry, 18 new languages, 29 integrations and a subject ID from the first page load.

In c15t 3.2, consent no longer makes your scripts late. When a visitor already gave consent, the scripts they allowed now start before the page hydrates.

This release also adds a shadcn registry, 18 new languages and 29 new consent-gated integrations. Each visitor now gets a subject ID on the first page load, and the backend records consent only from the sites you trust.

Allowed scripts start before hydration

When a visitor already gave consent, c15t starts their allowed scripts from the server HTML. The scripts do not wait for hydration. This works in Next.js, TanStack Start, Nuxt and SvelteKit.

Vendor helpers in @c15t/integrations that support early starts run before hydration. Other helpers load after hydration, as before.

Nothing starts early in two cases:

  • The policy asks the visitor to make a choice.
  • The page comes from prerendered or cached HTML.

To turn the early start off, set serverStart to false:

const options = {
  scriptLoader: { serverStart: false },
};

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

See Start allowed scripts before hydration.

Read how we built it in We built a cookie banner that makes your scripts start sooner.

Build the banner with shadcn

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 the shadcn components in your project. You own the markup and can change it. c15t keeps the consent logic, so you still get its fixes when you update c15t.

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 accepts 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.

The registry has no IAB TCF items yet. Under an IAB policy, the TCF banner from <IABProvider> shows instead of the shadcn banner.

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

Build your own banner. The registry items use public headless hooks. 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 the stock UI. See headless UI.

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

Show the banner in 53 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. The new languages are:

  • 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 language or all of them:

import '@c15t/translations/ko';
// or every language
import '@c15t/translations/all';

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. See translations.

Connect 29 more tools

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

GroupIntegrations
Analytics and visitor identificationFullStory, Gainsight PX, Lucky Orange, Pendo, Statable, RB2B, HubSpot, Default and Clay
Experiments and personalizationAdobe Target, Appcues, Dynamic Yield, Optimizely Web Experimentation and Wisepops
Email and CRMActiveCampaign, Attentive, Customer.io, Insider, Marketo Munchkin, Oracle Eloqua and Salesforce Account Engagement
Ads and pixelsAdRoll, Amazon Ads, LiveIntent, The Trade Desk and Vector
Chat and supportAda, Gorgias Chat and Zendesk Messaging

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

Three integrations from earlier releases also changed:

  • Databuddy now waits for measurement consent. It also supports optional session replay and tracking feature switches. See the Databuddy guide.
  • 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.

Get 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. c15t also keeps it when a visitor clears their choices or revokes consent.

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 when the visitor first loads a page. The server and the browser then start with the same ID. Other servers can use resolveRequestSubject() from c15t/server.

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

See subject IDs.

Other changes

Keep fixed elements still during a scroll lock. A banner or preference dialog that blocks the page can add space for the scrollbar. c15t now exposes that width as --c15t-scroll-lock-gap.

Add it to the right offset or padding of fixed headers and chat buttons. The padding also accounts for CSS zoom.

.site-header {
  position: fixed;
  inset-inline: 0;
  top: 0;
  padding-right: var(--c15t-scroll-lock-gap, 0px);
}

.chat-button {
  position: fixed;
  right: calc(1rem + var(--c15t-scroll-lock-gap, 0px));
  bottom: 1rem;
}

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

offline() is now local(). The new name makes it clear that choices stay in the browser.

Development builds now show the limits of local mode once per page. To hide that message, set acknowledgeNoRecords:

import { local } from 'c15t/react';

const mode = local({ acknowledgeNoRecords: true });

offline() still works until v4.

Clearer backend errors. When the backend refuses a call, the error now says why. In 3.1, an error over HTTP/2 could stop at /init responded 402.

Now the error 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.

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, such as Shopify's Dawn theme.
  • The c15t tab now supports Nuxt DevTools 4. Popup windows work too.
  • The dialog close-button offset and the 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 visitor clock that runs slightly fast no longer makes a consent save fail.
  • Invalid identity links return a validation error before the backend writes anything. A retry of an unchanged link no longer adds duplicate audit entries.

Check these details before you upgrade

  • Packages: Update c15t and all the @c15t/* packages you use to 3.2 together.
  • trustedOrigins: If you set it, make sure it lists every site that saves consent. Also list the site that hosts the backend. 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. If you add the site in that time, the backend still records its consent.
  • Cookie policy: If it lists c15t cookies, add c15t-sid and c15t-notice-subject. Their prefixes follow your custom storageKey.
  • Shared caches: If you use the Next.js proxy or SvelteKit handle, set subjectCookie: false for routes whose responses enter a shared cache. Also turn off server subject cookies when browser persistence is off or a consentSource owns consent. Give the server helper and the browser the same storage settings.
  • Your own API calls: If your 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.
  • Right-to-left banners: 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 it on the right for right-to-left pages, set position.
  • Kurdish: Plain ku now reads left to right, because it usually means Kurmanji. For Sorani, use ckb or ku-Arab.

Read the complete 3.2.0 release notes for every link and detail.