---
title: Islands
description: Choose whether React, Vue or Svelte renders the c15t preference
  dialog on an Astro site, and read consent from your own framework islands
  through the page's shared consent runtime.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## 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:

|`ui`|Dialog comes from|Install|
|--|--|--|
|`'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()`:

```js title="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.

## Use consent in your own islands

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:

```tsx title="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:

```astro title="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](/docs/frameworks/astro/client-api#read-a-permission-not-a-recorded-choice).

### Vue and Svelte islands

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

|Framework|Pass the runtime to|With|
|--|--|--|
|React|`ConsentProvider` from `c15t/react`|`runtime={runtime}` and `options={{ colorScheme: null }}`|
|Vue|`app.use(c15tVue, …)`, with `c15tVue` from `c15t/vue/vue-plugin`|`{ runtime, colorScheme: null }`|
|Svelte|`ConsentProvider` from `@c15t/svelte`|`runtime={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](/docs/frameworks/astro/embeds) 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.
