Skip to main content

Embeds

YouTube

Configure the video

Use the ID of an embeddable video and the category matching its purpose in your policy. This page uses marketing because the YouTube player can set advertising identifiers. The framework quickstarts gate their demo video on measurement instead; either works if your policy says so. The current v3 adapters use consent-gated iframes rather than the v2 YouTubeEmbed convenience component.

src/embed-config.ts
export const embedCategory = 'marketing' as const;
export const embedURL =
	'https://www.youtube-nocookie.com/embed/VIDEO_ID?playsinline=1';
export const embedTitle = 'Product walkthrough';
export const embedAspectRatio = '16 / 9';

Replace VIDEO_ID before running the example.

Add the embed to your framework

Create src/embed-config.ts using the configuration on this page, then select your framework. Keep the existing consent setup, its policy and preferences UI. These examples do not create a second consent provider.

Render this component inside your existing consent boundary or provider.

'use client';

import { ConsentGate } from 'c15t/next';
import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';

export function ConsentEmbed() {
  return (
    <ConsentGate category={embedCategory}>
      <iframe
        src={embedURL}
        title={embedTitle}
        loading="lazy"
        allowFullScreen
        style={{ width: '100%', aspectRatio: embedAspectRatio, minHeight: 200, border: 0 }}
      />
    </ConsentGate>
  );
}

ConsentGate keeps the iframe absent while permission is denied and removes it on revocation. Keep your existing consent styles and preferences dialog.

Browser helper for Astro and JavaScript

Only the Astro and JavaScript examples need this helper. It creates the iframe when permission allows it, keeps an existing player mounted across unrelated snapshot updates, and removes it on revocation.

src/consent-embed.ts
import {
	embedCategory,
	embedURL,
	embedTitle,
	embedAspectRatio,
} from './embed-config';

type EmbedKernel = {
	getSnapshot: () => {
		effectivePermissions: Record<typeof embedCategory, boolean>;
	};
	subscribe: (listener: () => void) => () => void;
};

export function mountConsentEmbed(
	container: HTMLElement,
	kernel: EmbedKernel,
	openPreferences: () => void
) {
	const render = () => {
		if (kernel.getSnapshot().effectivePermissions[embedCategory]) {
			if (container.querySelector('iframe')) return;
			const frame = document.createElement('iframe');
			frame.src = embedURL;
			frame.title = embedTitle;
			frame.loading = 'lazy';
			frame.allowFullscreen = true;
			Object.assign(frame.style, {
				width: '100%',
				aspectRatio: embedAspectRatio,
				minHeight: '200px',
				border: '0',
			});
			container.replaceChildren(frame);
		} else {
			if (container.querySelector('button')) return;
			const button = document.createElement('button');
			button.type = 'button';
			button.textContent = 'Open privacy settings to view this content';
			button.onclick = openPreferences;
			container.replaceChildren(button);
		}
	};

	render();
	const unsubscribe = kernel.subscribe(render);
	return () => {
		unsubscribe();
		container.replaceChildren();
	};
}

Keep a transcript, address or other useful alternative outside the embed. loading="lazy" is a performance hint; the consent condition controls whether the iframe exists at all. A denied category may be fixed by policy, so opening preferences does not guarantee that the visitor can grant it.

Gate existing iframe markup

For markup that uses data-category and data-src, the createIframeBlocker module from c15t/modules/iframe-blocker promotes data-src only when the category has consent and the URL resolves to HTTP or HTTPS. Relative URLs resolve against the iframe document's base URL. Malformed URLs and other schemes, including javascript:, data: and blob:, remain in data-src without loading. Correct the URL and call processAllIframes() to retry.

Keep the initial URL in data-src, without src, so the browser cannot load it before the blocker runs. The module does not sanitize arbitrary HTML or modify iframes without data-category.

Customize the player and placeholder

Set playback options in the iframe URL. For example, append start=36 to begin 36 seconds into the video. Use supported YouTube player parameters and reserve at least a 200-by-200-pixel player area.

React and Svelte ConsentGate components provide a blocked-state placeholder and consent action. Their placeholder APIs differ. React takes a node, and Svelte takes a snippet. Keep a clear way to reopen preferences when customizing them. The Vue and browser examples show that action explicitly. loading="lazy" delays an allowed iframe for performance; it does not implement consent gating.

Add data-vendor="youtube" next to data-category and declare a youtube vendor in the runtime's vendors option or the backend manifest to let visitors turn YouTube off while keeping the rest of the category on. See vendor consent for your framework.

Each example keeps the iframe absent until the category is allowed. After permission, the iframe can load. Revoking permission removes the iframe and its embedded document. Requests already sent cannot be recalled.

The youtube-nocookie.com hostname does not replace the consent boundary. Avoid loading a remote thumbnail, preconnecting to YouTube, or installing the IFrame Player API separately if you require no YouTube request before permission. This recipe embeds a video; it does not load the JavaScript Player API.

Verify the integration

In a fresh opt-in session, confirm no iframe or YouTube request exists. Grant marketing permission and play the video, then revoke it and confirm the iframe is removed. Test a narrow viewport and keyboard access to the placeholder and player. See consent verification.