---
title: Scripts
description: Register vendor scripts with @c15t/browser, createConsentRuntime or
  a consent kernel in JavaScript, and control what happens when a visitor
  withdraws permission.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Register scripts where you start c15t

Pass the `scripts` list to whichever API starts c15t. The list is the same in
each case, built from `@c15t/integrations` helpers or your own script objects:

|Setup|Where `scripts` goes|
|--|--|
|`@c15t/browser`|`init({ scripts, … })`, as in the [quickstart](/docs/frameworks/javascript/quickstart#start-c15t).|
|`c15t/runtime`|`createConsentRuntime({ mode, scripts })`, as in [headless](/docs/frameworks/javascript/headless#create-the-runtime).|
|Your own kernel|`createScriptLoader({ kernel, scripts })`. See [script loader](/docs/frameworks/javascript/modules/script-loader#attach-the-loader-to-your-own-kernel).|

The quickstart's `src/main.ts` shows PostHog:

```ts title="src/main.ts"
import { init, manifest } from '@c15t/browser';
import { posthog } from '@c15t/integrations/posthog';

init({
	mode: manifest(),
	scripts: [
		posthog({
			id: 'phc_your_project_key',
			initOptions: { cookieless_mode: 'never' },
			loadMode: 'after-consent',
		}),
	],
});
```

Each [integration guide](/docs/integrations/overview) gives the helper and
options for one vendor. For a vendor without a helper, write the object
yourself:

```ts
const scripts = [
	{
		id: 'analytics',
		src: 'https://analytics.example/sdk.js',
		category: 'measurement',
		onLoad: () => window.analytics?.track('page_view'),
	},
];
```

`window.analytics` stands for your vendor's global. The
[script loader](/docs/frameworks/javascript/modules/script-loader) page lists
every field and callback.

## When the script loader downloads

With `@c15t/browser` from npm, the script loader and the network blocker
share a separate chunk. It loads when c15t starts, and only if `scripts` is
not empty or `networkBlocker` has rules, so a page with neither never
downloads it. On a page with either, the browser requests it after your
JavaScript has run, which delays a returning visitor's consented scripts and
held requests by one request.

Your bundler names that chunk, so c15t cannot link it from your HTML. To
start the download earlier, add a `<link rel="modulepreload">` for the chunk
your build emits for `@c15t/core/dist/modules/loader-and-blocker.js`. With
Vite, `.vite/manifest.json` lists it under that path when `build.manifest` is
on. Give the link `fetchpriority="low"`. c15t needs the chunk only when it
starts, and at the default priority the preload can delay your app's own
chunks over HTTP/1.1. The script-tag build, `c15t.js`, already contains the
loader.

## Gate a snippet in your HTML

`@c15t/browser` also runs `<script type="text/plain" data-c15t-category>`
tags in your `index.html` once their category is allowed, in page order. Use
it for a vendor snippet you would rather keep in HTML. `createConsentRuntime`
and a plain kernel do not scan the page for these tags;
`activateGatedScripts(snapshot)` from `@c15t/browser` runs them for you.

With the `nonce` option set, `@c15t/browser` runs only the tags that carry the
same `nonce`, and marks the others `data-c15t-activated="untrusted"`. Pass
`{ nonce }` as the third argument of `activateGatedScripts` for the same
check. See [Content Security Policy](/docs/frameworks/javascript/content-security-policy).

## When a visitor withdraws permission

A script that has run cannot be unloaded. `@c15t/browser` and
`createConsentRuntime` reload the page after a visitor turns off a category
they had allowed, so the new page starts without that vendor. Allowing a
category never reloads.

Set `reloadOnConsentRevoked: false` to handle withdrawal yourself, for
example with the vendor's opt-out call in the script's `onConsentChange`.
Until the next page load, code that already ran keeps running.
`callbacks.onBeforeConsentRevocationReload` runs right before the reload, for
work that must finish first. A kernel you create yourself does not reload;
add `watchRevocationReload` from `c15t` if you want it.

## Keep one owner per vendor

Use stable script IDs and remove the vendor's original snippet, so each
vendor loads once and only through c15t. Ordinary scripts wait for
permission. Helpers with `alwaysLoad` load at once and pass consent to the
vendor's own API instead. Read the individual integration guide before
assuming all helpers have the same network behavior.

Create `init()` or the runtime once per page. A second client, or a second
loader on a kernel that `@c15t/browser` or `createConsentRuntime` owns, loads
vendors twice.

## Let visitors turn off one vendor

A visitor can allow marketing and still switch off one vendor in it. Declare
the vendors in the `vendors` option and render the switch yourself; helpers
from `@c15t/integrations` already carry their vendor slug. See
[vendor consent](/docs/frameworks/javascript/vendor-consent).

## Clear stored tracking data

Script gating does not remove cookies or Web Storage entries that a script
already wrote. Configure [clear on revocation](/docs/frameworks/javascript/clear-on-revocation)
to delete them when their category is denied.

## Send your own events to allowed vendors

To send your own analytics events only to the vendors a visitor allows, use
the event dispatcher from `@c15t/integrations`. See
[send events only to allowed integrations](/docs/integrations/overview#send-events-only-to-allowed-integrations).

## Check it works

Run the app in a private window with the Network tab open.

1. Filter for each vendor's host. Nothing loads before a choice.
2. Allow one category. Only that category's vendors load.
3. Withdraw it. The page reloads and the vendor stays absent.
