Skip to main content

JavaScript Modules

Script loader

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 shows where the list goes in each setup. The helpers in @c15t/integrations return Script objects for common vendors; each integration guide covers one.

Script fields

FieldTypeDefaultWhat it does
idstringrequiredA unique, stable name. The loader tracks the script by it.
categorycategory or conditionrequiredWhat must be allowed, such as 'measurement' or { and: ['measurement', 'marketing'] }.
srcstringnoneThe URL to load.
resourceKeystringidThe 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.
textContentstringnoneInline code to run instead of src.
callbackOnlybooleanfalseAdd no element, run only the callbacks. For a vendor already on the page.
alwaysLoadbooleanfalseLoad whatever the consent state, for tags that manage consent themselves through the vendor's API.
observeConsentBeforeLoadbooleanfalseNotify onConsentChange during initial denial and subsequent updates before the resource loads. Useful for integrations that coordinate a shared SDK.
persistAfterConsentRevokedbooleanfalseKeep the element after withdrawal instead of removing it.
target'head' | 'body''head'Where the element goes.
async, deferbooleannoneAttributes on the created element.
noncestringthe runtime's nonceCSP nonce for this element.
fetchPriority'high' | 'low' | 'auto'noneFetch priority hint.
attributesRecord<string, string>noneOther attributes for the element.
anonymizeIdbooleantrueGive the element a random id, so content blockers cannot match it by name.
vendorstringnoneA vendor slug for vendor-level consent. The script also waits while the visitor has turned that vendor off.
vendorId, iabPurposes, iabLegIntPurposes, iabSpecialFeaturesIAB IDsnoneUnder an IAB policy, the TC string decides instead of the category.

Lifecycle callbacks

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

CallbackWhen it runs
onBeforeLoadBefore a load attempt. It can run again for the same script if consent changes before the load starts, so make it safe to repeat.
onLoadAfter the script loaded. Use it for set-up that needs the vendor's code.
onErrorWhen the script failed to load.
onConsentChangeEach 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.
onDisposeWhen 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:

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.