Skip to main content

Vue Customization

Headless

When to go headless

Tokens, presentation and components slots change most designs without new markup. See customize first. Go headless when the consent UI has to be your own markup, such as a design system component.

Headless changes only the UI. The Vue plugin still resolves the policy, stores choices, loads your scripts and gates iframes.

Install the plugin as usual

Install c15tVue exactly as in the quickstart, with the Vite plugin and your scripts:

src/main.ts
import { c15tVue, manifest } from 'c15t/vue/vue-plugin';
import { createApp } from 'vue';

import App from './App.vue';
import { scripts } from './scripts';

createApp(App).use(c15tVue, { mode: manifest(), scripts }).mount('#app');

This component shows a banner while activeUI is 'banner' and a preference form while it is 'manager':

src/ConsentPrompt.vue
<script setup lang="ts">
import {
	useConsentActiveUI,
	useConsentDraft,
	useConsentSave,
} from 'c15t/vue/vue-plugin';

const activeUI = useConsentActiveUI();
const saveConsent = useConsentSave();
const { displayedCategories, save: saveDraft, values } = useConsentDraft();

// Recording a choice satisfies the prompt, so c15t closes the banner itself.
const choose = (choice: 'all' | 'none') => {
	void saveConsent(choice);
};

// Preferences stay open until you close them. c15t records the draft
// locally before the network request, so closing right away is safe.
const savePreferences = () => {
	void saveDraft();
	activeUI.value = null;
};
</script>

<template>
	<section
		v-if="activeUI === 'banner'"
		class="consent-prompt"
		aria-label="Cookie consent"
	>
		<p>We use cookies for measurement and marketing.</p>
		<button type="button" @click="choose('none')">Reject all</button>
		<button type="button" @click="choose('all')">Accept all</button>
		<button type="button" @click="activeUI = 'manager'">Customize</button>
	</section>
	<section
		v-else-if="activeUI === 'manager'"
		class="consent-prompt"
		aria-label="Privacy settings"
	>
		<label v-for="category in displayedCategories" :key="category">
			<input
				v-model="values[category]"
				type="checkbox"
				:disabled="category === 'necessary'"
			/>
			{{ category }}
		</label>
		<button type="button" @click="savePreferences">Save</button>
	</section>
</template>

useConsentSave() records Accept all or Reject all. Recording a choice satisfies the prompt, so c15t sets activeUI to null and the banner closes. useConsentDraft() holds the preference form. displayedCategories lists the categories the policy lets the visitor choose, values holds each switch, and save() records them. Preferences stay open after a save, so close them yourself. Composables lists every field of the draft.

Add focus handling, a close control and styles before you ship this. The component keeps the bare markup so the consent logic is easy to read.

Mount it instead of ConsentRoot

Render your component where ConsentRoot would go, and keep a way to reopen preferences:

src/App.vue
<script setup lang="ts">
import { useConsentActiveUI } from 'c15t/vue/vue-plugin';

import ConsentPrompt from './ConsentPrompt.vue';
import HomePage from './HomePage.vue';

const activeUI = useConsentActiveUI();
</script>

<template>
	<ConsentPrompt />
	<HomePage />
	<footer>
		<button type="button" @click="activeUI = 'manager'">
			Privacy settings
		</button>
	</footer>
</template>

Setting activeUI to 'manager' opens your preference form. Do not render ConsentRoot as well, or the visitor sees two banners.

Show the buttons the policy requires

A policy can require buttons and forbid others. For a notice, it requires a dismiss action instead of Accept and Reject. Read the resolved actions with useConsentPolicyActions('prompt') and render primaryActions and actionGroups from it, rather than hard-coding the buttons. Call useDismissNotice() for a notice's dismiss action. Dismissing records an acknowledgement, not consent.

usePromptRequirement() tells you whether the policy asks for a choice, a notice or nothing.

Verify the custom UI

Run through verify consent with your component. Check that Reject all is as easy to reach as Accept all, that the preference form lists the policy's categories, and that a rejection survives a reload.