---
title: Context getters
description: Reference for every @c15t/svelte context getter in SvelteKit
  components, from reading permissions and recorded choices to saving, the
  draft, IAB state and events.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Where getters work

Svelte has no consent hooks or stores. `@c15t/svelte` exports context getters
instead. Call one at the top level of a component's `<script>`, inside
`ConsentProvider`, and read its properties in markup or in `$derived`.
The properties read the provider's `$state`, so the component updates when
consent changes.

A getter uses Svelte's `getContext`, so it throws when you call it:

* in an event handler, an effect or a timer,
* in a component rendered outside the provider,
* in a plain `.ts` module.

Call it once at the top level and keep the returned object; its methods work
from event handlers later. The same getters are exported from
`@c15t/svelte/headless`.

|Getter|Returns|Use it to|
|--|--|--|
|`getConsentManager()`|`ConsentManagerState`|Read permissions and choices, and record choices. Start here.|
|`getHeadlessConsent()`|Surface state and actions|Build your own banner. See [headless](./headless).|
|`getIAB()`|`SvelteIABState` or `null`|Read or change IAB TCF consent.|
|`getConsentKernel()`|`ConsentKernel`|Subscribe to raw events or call low-level commands.|
|`getSnapshot()`|`ConsentSnapshot`|Read the full kernel snapshot once.|

## Read a permission or a recorded choice

A permission answers "may this run now?" A recorded choice answers "what did
the visitor decide?" They differ. Under an opt-out policy a category can be
allowed before the visitor chooses anything, and Global Privacy Control can
deny a category the visitor allowed. Load scripts and render optional
features from permissions; use the recorded choice only to show or report
what the visitor decided.

```svelte title="src/lib/consent-status.svelte"
<script lang="ts">
	import { getConsentManager } from '@c15t/svelte';

	// Call getters at the top level of the component, inside the provider.
	const consent = getConsentManager();

	// A permission: may measurement code run now?
	const measurementAllowed = $derived(consent.has('measurement'));
	// A recorded choice: what did the visitor decide? `null` until they choose.
	const measurementChoice = $derived(
		consent.explicitChoice?.categories.measurement?.value
	);
</script>

<dl>
	<dt>Measurement may run</dt>
	<dd data-testid="measurement-permission">
		{measurementAllowed ? 'Yes' : 'No'}
	</dd>
	<dt>Visitor's measurement choice</dt>
	<dd data-testid="measurement-choice">
		{measurementChoice === undefined
			? 'Not chosen'
			: measurementChoice
				? 'Allowed'
				: 'Denied'}
	</dd>
</dl>
```

Before the visitor chooses, this component shows "No" and "Not chosen".
[How consent works](/docs/concepts/how-consent-works#a-permission-is-not-a-recorded-choice)
explains the difference.

|Property or method|Answers|
|--|--|
|`has(condition)`|Whether a category, or a condition such as `{ and: ['measurement', 'marketing'] }` or `{ or: [...] }`, is allowed now.|
|`effectivePermissions`|Every category's permission, such as `effectivePermissions.measurement`.|
|`explicitChoice`|The visitor's recorded choice, or `null` before they choose. `explicitChoice.categories.measurement.value` is one category's answer.|
|`noticeDismissal`|Whether and when a notice was dismissed. Dismissing a notice is not consent.|
|`privacySignals`|Signals from the browser, such as `privacySignals.gpc.detected`.|
|`restrictions`|Per category, the reasons a recorded grant is overridden, such as GPC.|
|`vendorChoice`, `selectedVendors`|Recorded and draft grants for individual vendors outside IAB.|

## Read the policy and the UI state

|Property|Value|
|--|--|
|`hasPolicy`|`true` once a policy resolved for the visitor. Optional categories stay denied until then.|
|`policyPending`|`true` while the policy request is in flight.|
|`policyRule`|The resolved rule: `model`, `prompt`, `scope`, `rights` and more.|
|`resolution`|The resolution outcome, with `status` of `matched`, `failed` or another state.|
|`model`|`'opt-in'`, `'opt-out'`, `'iab'` or `'none'`.|
|`promptRequirement`|Whether the visitor owes a choice, a notice acknowledgement or nothing.|
|`hasConsentUi`|Whether the policy calls for any consent surface.|
|`hasConsentPreferences`|Whether a way back to preferences should show. `ConsentDialogLink` renders on this.|
|`activeUI`|`'banner'`, `'dialog'` or `'none'`.|
|`location`|The country and region the policy resolved for.|
|`overrides`|The forced country, region, language or GPC value, if any.|
|`consentCategories`, `consentTypes`|The categories the preference UI offers, as names and as full definitions.|
|`translationConfig`, `translations`|The active copy and language.|
|`legalLinks`, `presentation`|The provider's `legalLinks` and `presentation` props.|
|`user`, `subject`|The identified user and the backend's subject for this visitor.|

## Record a choice

Call the methods from event handlers. Save methods return a promise.

```svelte title="src/lib/privacy-controls.svelte"
<script lang="ts">
	import { ConsentDialog, getConsentManager } from '@c15t/svelte';

	const consent = getConsentManager();

	// Record one category from your own button. setConsent changes the draft;
	// saveConsents('custom') records it.
	const allowAnalytics = async () => {
		consent.setConsent('measurement', true);
		await consent.saveConsents('custom');
	};
</script>

<button type="button" onclick={() => consent.setActiveUI('dialog')}>
	Privacy settings
</button>
<button type="button" onclick={allowAnalytics}> Allow analytics </button>
<ConsentDialog />
```

Clicking **Allow analytics** records measurement as allowed and leaves the
other categories as they were. **Privacy settings** opens the dialog.

|Method|Effect|
|--|--|
|`setActiveUI(surface)`|`'dialog'` opens preferences, `'banner'` shows the banner, `'none'` closes the open surface. Closing preferences brings the banner back while the policy still owes a choice. Records nothing.|
|`setConsent(category, value)`|Changes one category in the unsaved draft. `setSelectedConsent` is the same method.|
|`setSelectedVendor(vendorId, granted)`|Changes one vendor in the draft. Ignored for undeclared or disabled vendors.|
|`saveConsents('custom')`|Records the draft. Throws when the policy changed since the draft started; see [the draft](#work-with-the-draft).|
|`saveConsents('all')`|Records every category in scope as allowed.|
|`saveConsents('necessary')`|Records only `necessary`, rejecting the rest.|
|`dismissNotice()`|Records that the visitor dismissed a notice. Only works while a notice is due.|
|`setLanguage(code)`|Switches the language and resolves the policy again for it.|
|`getDisplayedConsents()`|The categories to render in your own preference UI.|
|`getDisplayedVendors(category)`|The vendors to list under one category. Empty under an IAB policy.|
|`isVendorAllowed(vendorId)`|`true` while a declared vendor's category is allowed and the visitor has not switched it off. `false` for an id nothing declares.|
|`subscribeToConsentChanges(listener)`|Calls `listener` with the permissions after every snapshot change. Returns an unsubscribe function.|

A save updates permissions in the same task, so registered scripts and
`ConsentGate` react before the backend request finishes. A failed request is
kept and retried; it does not undo the choice, and the provider's `onError`
callback reports it. When a save withdraws a category that was granted, the
provider reloads the page by default so code that already ran is gone. Set
`reloadOnConsentRevoked={false}` on the provider to handle that yourself.

Only call save methods from a visitor's action. Saving on page load records a
choice the visitor never made.

## Work with the draft

`consent.draft` is the unsaved state every preference surface on the page
shares: the dialog, `ConsentWidget` and your own UI.

|Member|Behavior|
|--|--|
|`values`|Each category's draft value. Starts from the recorded choice, or from the policy's defaults before a choice. A choice saved elsewhere moves the values the visitor has not touched. Categories outside `consentCategories` read `false`. `selectedConsents` is the same object.|
|`vendors`|Each declared vendor's draft grant.|
|`isStale`|`true` when the policy, the displayed categories or the vendor list changed while the draft held an unsaved change. A draft with no unsaved change follows the policy and is never stale.|
|`set(category, value)`, `setVendor(id, granted)`|The same as `setConsent` and `setSelectedVendor`.|
|`reset()`|Discards unsaved changes.|

The draft's code loads with the first preference surface, so a page that
only shows the banner does not ship it. `ConsentWidget` and `ConsentDialog`
bring it with them, and their switches render seeded on the first render,
on the server too. Headless code that reads or writes the draft without
either one starts the load itself: until it lands, `values` and `vendors`
are empty objects and `isStale` is `false`, and they update reactively once
it does. Writes made before then apply in order, `draft.reset()` drops
them, and `saveConsents('custom')` waits for the draft before it records.

When `isStale` is `true`, `saveConsents('custom')` throws "The policy changed.
Review your preferences before saving." Show the visitor the current
categories, call `draft.reset()`, and let them save again.

## Read and change IAB TCF consent

`getConsentManager().iab` is the live IAB state, or `null` when IAB is off.
`getIAB()` returns the same object at the moment you call it; read `.iab` on
the manager for values that update.

|Member|Value or effect|
|--|--|
|`config.enabled`, `config.cmpId`|Whether the add-on is on, and the CMP ID.|
|`gvl`, `isLoadingGVL`|The Global Vendor List, and whether it is still loading.|
|`purposeConsents`, `vendorConsents`, `specialFeatureOptIns`|The visitor's TCF choices, staged or saved.|
|`tcString`|The saved TC String, or `null`.|
|`nonIABVendors`|Custom vendors outside the IAB list.|
|`acceptAll()`, `rejectAll()`|Accept All sets vendor and purpose consent for declared consent purposes, separate vendor and purpose legitimate-interest signals for declared legitimate-interest purposes, and opt-ins for declared special features. Reject All clears these signals.|
|`setPurposeConsent(id, value)`, `setPurposeLegitimateInterest(id, value)`|Change one purpose.|
|`setVendorConsent(id, value)`, `setVendorLegitimateInterest(id, value)`|Change one vendor.|
|`setSpecialFeatureOptIn(id, value)`|Change one special feature.|
|`save()`|Writes the TC String and records the choice.|
|`preferenceCenterTab`, `setPreferenceCenterTab(tab)`|The tab `IABConsentDialog` shows.|

Until the TCF add-on finishes loading, the action methods queue and replay.
See [IAB TCF](./iab) for setup.

## Listen to consent events

`getConsentKernel()` returns the consent kernel behind the provider. Its
`events.on(type, listener)` subscribes to one event and returns an
unsubscribe function. Subscribe in `$effect` so Svelte removes the listener
when the component unmounts:

```svelte title="src/lib/consent-events.svelte"
<script lang="ts">
	import { getConsentKernel } from '@c15t/svelte';

	// Read the kernel at the top level; subscribe in an effect so the
	// listener is removed when the component unmounts.
	const kernel = getConsentKernel();
	let lastRecorded = $state<string | null>(null);

	$effect(() =>
		kernel.events.on('choice:recorded', ({ snapshot }) => {
			const { measurement } = snapshot.effectivePermissions;
			lastRecorded = measurement ? 'measurement allowed' : 'measurement denied';
		})
	);
</script>

<p data-testid="last-recorded">
	{lastRecorded ?? 'No choice recorded on this page yet'}
</p>
```

`choice:recorded` fires for an explicit accept, reject or save.
`permissions:changed` fires for any change to what may run. For callbacks that
cover the whole app, use the provider's `callbacks` prop instead; see
[callbacks](./callbacks).

The kernel also has `subscribe(listener)`, which runs on every snapshot
change, `getSnapshot()`, and `commands` for `init()`, `save()`,
`dismissNotice()` and `identify()`. Prefer the manager's methods; they keep
the draft and the open surface in step, which the raw commands do not.
`getSnapshot()` from `@c15t/svelte` returns the snapshot at the moment you
call it and does not update.

## Other exports

|Export|Purpose|
|--|--|
|`manifest()`|The `mode` that resolves the policy in the browser from the manifest `consentManifest()` downloaded.|
|`hosted({ backendURL })`|The `mode` that asks an Inth or self-hosted backend's `/init` for every visitor.|
|`offline({ policyRules })`|The `mode` that resolves policies in the browser with no backend. Not recommended for production environments.|
|`custom(transport)`|The `mode` for your own transport.|
|`createConsentRuntime(options)`|Creates a runtime to share between providers, through their `runtime` prop.|
|`resolveConsentPresentation(input)`|Resolves the banner or dialog shape and actions a policy asks for, as `getHeadlessConsent()` does.|
|`defaultTranslationConfig`, `mergeTranslationConfigs`, `prepareTranslationConfig`, `detectBrowserLanguage`|Translation helpers.|

## Read consent on the server

Getters run in components. On the server there is no provider, only the
state `loadConsent` returns, or `event.locals.c15t.config` when `c15tHandle`
is installed. That state is the input the provider starts from. It holds the
stored records, the request's location and GPC, and the resolved policy when
the backend answered. It holds no computed permissions, so gate vendors in the browser
through `scripts` and the getters on this page.
[Rendering and deployment](/docs/frameworks/sveltekit/rendering#resolve-request-context-once-with-c15thandle)
covers `c15tHandle`.
