Skip to main content

Astro Advanced

Islands

How the dialog islands work

The c15t banner is plain .astro markup. The preference dialog and the IAB TCF dialog need component state, so each one is an island in a UI framework. The integration's ui option picks that framework:

uiDialog comes fromInstall
'svelte' (default)@c15t/svelte, bundled with c15t@astrojs/svelte svelte
'react'c15t/react@astrojs/react react react-dom
'vue'c15t/vue@astrojs/vue vue

The island is not a client:load island. c15t mounts it into ConsentDialog's empty host the first time something opens the dialog. A visitor who only accepts or rejects from the banner downloads no framework code for the dialog at all.

Pick the framework your site ships

The dialog should reuse the framework runtime visitors already download. With ui unset, the integration picks the framework of the one Astro integration among @astrojs/svelte, @astrojs/react and @astrojs/vue your site registers. With none or several, it picks Svelte, whose runtime is the smallest of the three; set ui yourself in that case.

Register the framework's Astro integration before c15t():

astro.config.mjs (partial)
integrations: [react(), c15t()],

The build stops when that integration is missing, and names the packages to install. Only the framework you name in ui ends up in your build. With ui: 'svelte' the build never resolves React or Vue.

A site that never opens a dialog, such as one that only shows a notice, can set requireUIIntegration: false and install no framework.

Your own React, Vue or Svelte islands can read consent with their adapter's hooks. Give the adapter the page's runtime, from getConsentClient()?.runtime, instead of letting it create a second one. A second runtime would keep its own copy of consent and fall out of step with the banner.

This React island shows whether measurement is allowed and opens the preference dialog:

src/components/consent-status.tsx
import { getConsentClient, openDialog } from 'c15t/astro/client';
import { ConsentProvider, useConsent } from 'c15t/react';

const MeasurementStatus = () => {
	const allowed = useConsent('measurement');
	return (
		<p>
			<span data-testid="measurement-status">
				Measurement is {allowed ? 'allowed' : 'not allowed'}.
			</span>{' '}
			<button
				onClick={() => {
					void openDialog();
				}}
				type="button"
			>
				Change cookie settings
			</button>
		</p>
	);
};

const ConsentStatus = () => {
	// The page's consent runtime, which the c15t integration started.
	const runtime = getConsentClient()?.runtime;
	if (!runtime) {
		return null;
	}
	return (
		// `colorScheme: null` leaves dark mode to the integration.
		<ConsentProvider options={{ colorScheme: null }} runtime={runtime}>
			<MeasurementStatus />
		</ConsentProvider>
	);
};

export default ConsentStatus;

Render it without server rendering, so it runs only in the browser, where the runtime exists:

src/pages/index.astro (partial)
<ConsentStatus client:only="react" />

Three details matter:

  • Borrow the runtime. With runtime, ConsentProvider neither starts nor disposes the runtime. The page owns it.
  • Pass colorScheme: null. Without it, the adapter manages the c15t-dark class itself and can undo the integration's colorScheme.
  • Open dialogs through c15t/astro/client. Call openDialog() to open preferences. An adapter's own dialog state, such as useSetActiveUI() in React, does not mount the Astro dialog island.

The island needs c15t/react and its Astro integration even when ui is 'svelte'. useConsent() reads a permission. To show the visitor's recorded choice, read explicitChoice from the snapshot. See read a permission, not a recorded choice.

Vue and Svelte islands

The Vue and Svelte adapters take the page runtime the same way:

FrameworkPass the runtime toWith
ReactConsentProvider from c15t/reactruntime={runtime} and options={{ colorScheme: null }}
Vueapp.use(c15tVue, …), with c15tVue from c15t/vue/vue-plugin{ runtime, colorScheme: null }
SvelteConsentProvider from @c15t/svelteruntime={runtime} and options={{ colorScheme: null }}

For Vue, an island's app is set up in the appEntrypoint module of @astrojs/vue. Install the plugin there, and only when getConsentClient() returns a client, because the entrypoint also runs on the server. The Vue components ship as .vue files, which Vite's dependency pre-bundling cannot load. The integration keeps @c15t/vue out of pre-bundling with ui: 'vue'. With another ui, add vite: { optimizeDeps: { exclude: ['@c15t/vue', 'c15t'] } } to astro.config.mjs yourself.

For Svelte, install @c15t/svelte@alpha, because the c15t package has no Svelte export.

Render Vue and Svelte islands that read consent with client:only, for the same reason as the React one.

Without a framework

A plain <script> or a custom element needs no adapter. Use subscribe() and getConsent() from c15t/astro/client, as the video component does.

Check the islands

  1. Build the site and open a page in a private window with DevTools Network open. Before you open the dialog, no chunk for the ui framework's dialog loads.
  2. Hover Privacy settings. The dialog chunks download.
  3. Open the dialog, turn on Analytics (the measurement category) and save. Your own island updates to match without a reload.
  4. Toggle your system's dark mode. The banner and the dialog follow the integration's colorScheme, not your island.