Skip to main content

Nuxt 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 Nuxt module still resolves the policy, stores choices, loads scripts from app.config.ts and gates iframes.

This component shows a banner while activeUI is 'banner' and a preference form while it is 'manager'. The module auto-imports the composables.

app/components/ConsentPrompt.vue
<script setup lang="ts">
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

Replace <ConsentRoot /> in app.vue with your component. Keep one way to reopen preferences, such as ConsentDialogLink.

app/app.vue
<template>
	<ConsentPrompt />
	<NuxtPage />
	<footer>
		<ConsentDialogLink />
	</footer>
</template>

When the server resolves the policy, in the default manifest() mode or hosted(), the component renders on the server with the visitor's policy, like ConsentRoot.

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.