---
title: Script loader
description: Reference for the c15t script loader in JavaScript, covering every
  Script field, lifecycle callbacks, updateScripts, disposal and
  createScriptLoader for a kernel you own.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## What the script loader does

The script loader adds a vendor's `<script>` to the page once the script's
category is allowed, and removes the element again when the visitor withdraws
it. `@c15t/browser` and `createConsentRuntime` create one from their `scripts`
option. Create one yourself only for a kernel you built with
`createConsentKernel`.

The [scripts guide](/docs/frameworks/javascript/scripts) shows where the list
goes in each setup. The helpers in `@c15t/integrations` return `Script` objects for
common vendors; each [integration guide](/docs/integrations/overview) covers
one.

## Script fields

|Field|Type|Default|What it does|
|--|--|--|--|
|`id`|`string`|required|A unique, stable name. The loader tracks the script by it.|
|`category`|category or condition|required|What must be allowed, such as `'measurement'` or `{ and: ['measurement', 'marketing'] }`.|
|`src`|`string`|none|The URL to load.|
|`resourceKey`|`string`|`id`|The DOM resource identity. Matching keys share one element within and across loaders, while each logical script keeps its callbacks and consent state. Use different keys for different resources under the same script ID.|
|`textContent`|`string`|none|Inline code to run instead of `src`.|
|`callbackOnly`|`boolean`|`false`|Add no element, run only the callbacks. For a vendor already on the page.|
|`alwaysLoad`|`boolean`|`false`|Load whatever the consent state, for tags that manage consent themselves through the vendor's API.|
|`observeConsentBeforeLoad`|`boolean`|`false`|Notify `onConsentChange` during initial denial and subsequent updates before the resource loads. Useful for integrations that coordinate a shared SDK.|
|`persistAfterConsentRevoked`|`boolean`|`false`|Keep the element after withdrawal instead of removing it.|
|`target`|`'head' \|'body'`|`'head'`|Where the element goes.|
|`async`, `defer`|`boolean`|none|Attributes on the created element.|
|`nonce`|`string`|the runtime's `nonce`|CSP nonce for this element.|
|`fetchPriority`|`'high' \|'low' \|'auto'`|none|Fetch priority hint.|
|`attributes`|`Record<string, string>`|none|Other attributes for the element.|
|`anonymizeId`|`boolean`|`true`|Give the element a random `id`, so content blockers cannot match it by name.|
|`vendor`|`string`|none|A vendor slug for vendor-level consent. The script also waits while the visitor has turned that vendor off.|
|`vendorId`, `iabPurposes`, `iabLegIntPurposes`, `iabSpecialFeatures`|IAB IDs|none|Under an IAB policy, the TC string decides instead of the category.|

## Lifecycle callbacks

Each callback receives `{ id, elementId, hasConsent, consents, element?, error?, vendor? }`.

|Callback|When it runs|
|--|--|
|`onBeforeLoad`|Before a load attempt. It can run again for the same script if consent changes before the load starts, so make it safe to repeat.|
|`onLoad`|After the script loaded. Use it for set-up that needs the vendor's code.|
|`onError`|When the script failed to load.|
|`onConsentChange`|Each time a loaded script's consent changes. With `observeConsentBeforeLoad`, also during initial denial and later changes before loading. Use it for a vendor's opt-out call when you turn off the reload.|
|`onDispose`|When the configuration is removed or the loader is disposed, including a script that never loaded.|

Withdrawal alone does not call `onDispose`. With the default reload, the page
reloads after withdrawal anyway; with `reloadOnConsentRevoked: false`, use
`onConsentChange` to stop the vendor.

When scripts share an element, each active registration receives its own
`onLoad` or `onError` callback. A registration that joins after c15t observed
completion receives that callback in a microtask, after its consent callback,
using its current callbacks and consent. Inline scripts share their deferred
completion too. Removing a registration cancels its pending callback. c15t does
not infer completion for elements added outside the loader.

Removing the original loader transfers ownership to a remaining loader. c15t
removes an element it created when the last loader releases it.

## Update the list

`loader.updateScripts(next)` replaces the configuration and reconciles at
once:

* A script with the same `id`, source, inline code and attributes keeps its
  element. New callback functions alone do not reload it.
* Changing the source, inline code or attributes starts a new load.
* A script that adds `onDispose` is tracked by object. Replacing the object
  disposes it and starts again, even with the same `id`. Keep such objects
  stable.
* A removed script releases its element. Shared external elements stay while
  another loader still uses them.

Updates requested from inside a callback run after the current pass; the
latest wins. A chain of callbacks that keeps changing consent or the list is
stopped after 100 passes, and the loader disposes itself.

`loader.getLoadedScriptIds()` lists the scripts currently loaded.

## Attach the loader to your own kernel

A kernel from `createConsentKernel` has no loader. This file creates one with
persistence, before initialization:

```ts title="src/consent.ts"
import { createConsentKernel, createHostedTransport } from 'c15t';
import { createPersistence } from 'c15t/modules/persistence';
import { createScriptLoader } from 'c15t/modules/script-loader';

import { scripts } from './scripts';

export const startConsentKernel = async function startConsentKernel(
	backendURL: string
) {
	const kernel = createConsentKernel({
		transport: createHostedTransport({ backendURL }),
	});
	// Each module is yours to create and dispose. This kernel has no iframe
	// blocker, network blocker, data clearing or reload after revocation.
	const persistence = createPersistence({ kernel });
	const loader = createScriptLoader({ kernel, scripts });
	await kernel.commands.init();
	return {
		dispose() {
			loader.dispose();
			persistence.dispose();
			kernel.dispose();
		},
		kernel,
	};
};
```

`createScriptLoader({ kernel, scripts, nonce?, onDebug? })` returns the
handle. `onDebug` receives every lifecycle step, for logging. Dispose the
loader before the kernel. Do not attach a loader to a kernel that
`@c15t/browser` or `createConsentRuntime` owns; each already has one.

## Check it works

1. Filter the Network tab for the vendor's host. Nothing loads before a
   choice.
2. Allow the category. The vendor loads and `onLoad` runs. In the Elements
   panel, the script sits in `<head>` with a random `id`.
3. With `reloadOnConsentRevoked: false`, withdraw the category.
   `onConsentChange` runs with `hasConsent: false` and the element is gone.
